I am currently working on a data visualization which takes information from a movie, that plays in realtime and creates simultaneously.
For that I want to have two seperate sketch windows. One window should show the movie playing in realtime and the other the ongoing visualization.
I can't seem to figure out how to easily add another sketch window and tried out some examples which are not working in Processing 3 anymore.
(sojamos library Control p5 for example)
Then I stumbled upon this example:
https://gist.github.com/atduskgreg/666e46c8408e2a33b09a
Eventhough I can get it to display two sketch windows at a time, I obviously cannot use the data from one window on the other one.
Is there another way to do this?
Thank you!
Nothing prevents you from declaring a function in the PWindow class (which creates a second window) which takes arguments you could use inside and call it from the other sketch.
So you can pass data in form as function arguments to the second window.
This small example passes the relative mousePressed position from the first window to the second via a function (here called evokedFromPrimary) and stores it in an ArrayList which draws them in the second window:
PWindow win;
public void settings() {
size(320, 240);
}
void setup() {
win = new PWindow();
}
void draw() {
background(255, 0, 0);
text("Click in this window to draw at a relative position in the other window", 10, 10, this.width - 20, 100);
}
void mousePressed() {
println("mousePressed in primary window");
float relPosX = map(mouseX, 0, this.width, 0, 100);
float relPosY = map(mouseY, 0, this.height, 0, 100);
win.evokedFromPrimary(relPosX, relPosY);
}
class PWindow extends PApplet {
ArrayList<PVector> vectors = new ArrayList<PVector>();
PWindow() {
super();
PApplet.runSketch(new String[] {this.getClass().getSimpleName()}, this);
}
void evokedFromPrimary(float relPosX, float relPosY) {
println("evoked from primary");
float xPos = map(relPosX, 0, 100, 0, this.width);
float yPos = map(relPosY, 0, 100, 0, this.height);
vectors.add(new PVector(xPos, yPos));
}
void settings() {
size(500, 200);
}
void setup() {
background(150);
}
void draw() {
background(150);
//store the vector size before using to avoid ConcurrentModificationException
int listLength = vectors.size();
for(int i = 0; i < listLength; i++) {
PVector v = vectors.get(i);
ellipse(v.x, v.y, random(50), random(50));
}
}
void mousePressed() {
println("mousePressed in secondary window");
}
}
The Pwindow code here is in the same .pda file.
Related
I'm making a gravity simulator and I need it animate live so the user can watch it. I've been able to make it trace out the path the object would take.
But as you can see it just traces it out and then displays the window. I think my problem is because all of this in the section of code that builds the JPanel but I don't know how to change it properly.
Here's what I'm doing for my window:
import java.awt.*;
import javax.swing.*;
import java.lang.Math;
public class Universe {
public static void main(String[] args) throws InterruptedException {
new Universe();
}
public Universe() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Gravity Simulator");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
}
int paneWidth = 500;
int paneHeight = 500;
#Override
public Dimension getPreferredSize() {
return new Dimension(paneWidth, paneHeight);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int size = Math.min(getWidth()-4, getHeight()-4) / 10;
int width = getWidth() - (size * 2);
int height = getHeight() - (size * 2);
int x0=paneWidth/2; int y0=paneHeight/2; int radius0=20;
int y = (getHeight() - (size * 10)) / 2;
for (int horz = 0; horz < 2; horz++) {
int x = (getWidth() - (size * 10)) / 2;
for (int vert = 0; vert < 10; vert++) {
g.drawRect(x, y, size, size);
drawCircle(g, x+25, y+25, 5);//A massive object would go here this just proof of concept
x += size;
}
y += size;
}
double[] velocity={5,-2};
MassiveObject planet = new MassiveObject(g, 20, 50, velocity, 250, 150);
planet.draw(g);
MassiveObject rock = new MassiveObject(g, 2, 25, velocity, 275, 300);
rock.draw(g);
double sGravity = fGrav(planet, rock);
//double dis = massDis(planet, rock);
System.out.println("Distance: "+massDis(planet, rock));
System.out.println("Gravity: "+sGravity+" Newtons of force(gravity is multiplied by "+1000000+")");
double[] traj = objectTrajetory(planet, rock, rock.getMass());
int t = 0;
try {
while(true) {
//double k = sGravity/dis;
//x and y components of motion
double xm = traj[0];
double ym = traj[1];
double[] nVelocity= {xm,ym};
//////////////////////////////
//set new position of object
rock.setX(rock.getX()+(xm));
rock.setY(rock.getY()+(ym));
rock.setVelocity(nVelocity);
rock.draw(g);
t++;
System.out.println("position changed: "+rock.getCoords());
traj = objectTrajetory(planet, rock, 1);
Thread.sleep(100);
if (t> 15){break;}
}
}
catch(Exception e) {
}
//System.out.println("Distance: "+massDis(planet, rock));
//System.out.println("Gravity: "+fGrav(planet, rock)+" Newtons of force(gravity is multiplied by "+1000000+")");
g2d.dispose();
}
And here is the code for the draw function of my MassiveObject:
public void draw(Graphics g){
Graphics2D g2d = (Graphics2D) g;
Ellipse2D.Double circle = new Ellipse2D.Double(this.x0-(this.radius/2), this.y0-(this.radius/2), this.radius, this.radius);
g2d.setColor(Color.GRAY);
g2d.fill(circle);
}
So basically what I'm asking is how can I make it run that algorithm to paste the MassiveObject at its new location after the window is already pulled up so the user can watch it happening instead of it just building the window with it already on it?
The logic of your animation shouldn't be in the paintComponent() method. The paintComponent() method should just paint the current frame of animation. The code inside paintComponent() is run inside a special thread dedicated to handling all UI paints, responding to clicks etc. So for as long as paintComponent() is running, nothing else can happen in the UI, hence your application "grinds to a halt".
The logic to periodically update the state and then order a repaint should be in a separate thread (or the main thread). When it has updated the state and needs the next frame to be drawn, it then calls the panel's repaint() method. Because you're doing this in another thread, you would surround it in SwingUtilities.invokeLater(). This orders Swing to to call back into the paintComponent():
while (true) {
// Update state used by the paintComponent() method
updateObjectPositions();
// Now draw the new animation frame
SwingUtilities.invokeLater(() -> {
universePanel.repaint(0, 0, universeWidth, universeHeight);
});
Thread.sleep(...);
}
Because the drawing and updating are happening in different threads, you need to make sure that the data is shared between the threads in a thread-safe way. If you're just starting out and the calculations are very quick, then you could put the updateObjectPositions() method inside the invokeLater() so that the update to the data and the redraw happen in the UI thread. But remember that the code inside the invokeLater() will be blocking the UI for as long as it runs, so it should be as brief as possible and just handle a single frame. Crucially, your while loop and sleep should not go inside the invokeLater() or inside any UI code such as paintComponent().
Thanks a lot for the help, I was able to get the program animating the way I wanted it to and it was exactly as you all suggested. I removed my logic from the paintComponent() and put it inside the JPanel pane, ran a timer to continuously update the position, and then ran the repaint() function at the end of each loop in timer.
public class TestPane extends JPanel {
int paneWidth = 1200;
int paneHeight = 1200;
double[] velocity={4,4};
MassiveObject planet = new MassiveObject( 50, 50, velocity, paneWidth/2,paneHeight/2);
MassiveObject rock = new MassiveObject( 2, 25, velocity, 150, 200);
double[] traj = objectTrajetory(planet, rock, rock.getMass());
double xm=0.00;
double ym=0.00;
public TestPane() {
Timer timer = new Timer(500, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
xm = traj[0];
ym = traj[1];
double[] nVelocity= {xm,ym};
//////////////////////////////
//set new position of object
rock.setX(rock.getX()+(xm));
rock.setY(rock.getY()+(ym));
rock.setVelocity(nVelocity);
System.out.println("position changed: "+rock.getCoords());
repaint();
traj = objectTrajetory(planet, rock, 1);
rock.setX(rock.getX()+(xm));
rock.setY(rock.getY()+(ym));
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(paneWidth, paneHeight);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int size = Math.min(getWidth()-4, getHeight()-4) / 10;
int width = getWidth() - (size * 2);
int height = getHeight() - (size * 2);
int x0=paneWidth/2; int y0=paneHeight/2; int radius0=20;
rock.draw(g);
planet.draw(g);
g2d.dispose();
}
The program now animates pretty smoothly instead of just spitting out a plot of the path it would take.
Snap of Animated Orbit
I have looked into Double Buffering and plan on implementing it eventually but as of right now I can't figure out how to use it or anything like it. I am trying to make pong so I plan on adding three objects total but for now I just want to get one object to work smoothly. I'm fairly new to graphics so I don't know entirely what I'm doing and I'm just trying to learn as I go.
Here is my code:
Pong:
public static void main(String[]args) {
JFrame window= new JFrame();
window.setTitle("Pong Game");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setPreferredSize(new Dimension(800,500));
window.pack();
window.setVisible(true);
Ball ball= new Ball();
Paddle player= new Paddle();
window.getContentPane().add(ball);
for(;;) {
ball.move();
//window.setContentPane(ball);
window.setContentPane(player);
player.move();
}
}
Paddles:
double x, y, ymove;
boolean cpu;
public Paddle() {
x=5;
y=180;
ymove=.1;
}
//passing an integer through to make the computer paddle
public Paddle(int a) {
cpu= true;
x=761;
y=180;
ymove=.1;
}
public void paint(Graphics g) {
g.setColor(Color.blue);
g.fillRect((int)x, (int)y, 18, 120);
}
public void move() {
y+=ymove;
if(y>=500-160||y<=0) {
ymove*=-1;
}
}
Ball:
double x, y, xspeed, yspeed;
public Ball() {
x=200;
y=200;
xspeed=0;
yspeed=.1;
}
public void move() {
x+=xspeed;
y+=yspeed;
if(y>=440||y<=0) {
yspeed*=-1;
}
}
public void paint(Graphics g) {
g.setColor(Color.black);
g.fillOval((int)x, (int)y, 20, 20);
}
This Answer is a very very simplified explanation! I recommend checking out this linux journal, which explores something similia.
The main issue, which causes the "flickering" is, that the draw is done "to fast".
Take your main loop:
for(;;) {
ball.move();
window.setContentPane(ball);
window.setContentPane(player);
player.move();
}
This loop updates the positions of the ball and afterwards "adds it to the content pane". While it is drawn, the next image is already added and drawn. This is causing the flickering (again, note: this is very simplified).
The simplest solution to fix the "flickering" is, to let the Thread sleep after it has drawn and "wait" until the draw is finished.
boolean running = true;
int delay = 15; // adjust the delay
while(running) {
ball.move();
player.move();
window.setContentPane(ball);
window.setContentPane(player);
try {
Thread.sleep(delay);
} catch(InterruptedException e) {
// We were interrupted while waiting
// Something "woke us up". Stop the loop.
e.printStackTrace();
running = false;
}
}
This Thread.sleep method let's the current Thread "wait" for the specified time.
The delay can be adjusted to something more practical. You could for example calculate how many frames you want and sleep for that amount.
Another way would be to "time" the updates. This could be done with a timer. Since it is more or less deprecated, i implement it using the ScheduledExecutorService
int delay = 15; // adjust the delay
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool();
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
ball.move();
player.move();
}
}, delay, TimeUnit.MILLISECONDS)
As of Java8, you might write this using a Lambda like this:
scheduledExecutorService.scheduleAtFixedRate(() -> {
ball.move();
player.move();
}, delay, TimeUnit.MILLISECONDS)
To stop it, you could now call:
scheduledExecutorService.shutdown();
However: There are more sophisticated solutions. One is, as you already noted, the double buffering. But there are also multiple different techniques, that compensate more difficult problems. They use something called page flipping.
The problem you are having is that you have split your paint methods,
if you make one class that is dedicated to doing the painting and you put all of your paints in one paint method it should work without flickering.
I would also recommend looking into making your paint calls run on a timer which lets you decide the refresh rate which usually leads to a smoother experience overall.
Here is an example of my graphics class in my latest game,
class GameGraphics extends JPanel implements ActionListener {
private Timer refreshHZTimer;
private int refreshHZ = 10;
private int frameID = 0;
public GameGraphics(int width, int height) {
setBounds(0,0, width, height);
setVisible(true);
refreshHZTimer = new Timer(refreshHZ, this);
refreshHZTimer.start();
}
#Override
public void actionPerformed(ActionEvent e) {
frameID++;
if (frameID % 100 == 1)
System.out.println("Painting FrameID: " + frameID);
repaint();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//Init graphics
Graphics2D g2 = (Graphics2D) g;
//Paint stuff here:
}
}
And add this code to your JFrame constructor:
GameGraphics gpu = new GameGraphics(width, height);
frame.add(gpu);
I have problem with animation in android.
My animations working good only on 1080x1920 480dpi display. But i don't know how made app for any display.
If I use method from http://www.i-programmer.info/programming/android/8797-android-adventures-beginning-bitmap-graphics.html?start=4 Animations run, ok but made shadows, so no rewrite display if I write on display something I not delete thise from display.
If I use method. Main activity have one threat
public class UpdateThread implements Runnable {
#Override
public void run() {
while(bez){
try {
myThread.sleep(50);
} catch (Exception ex) {}
MainActivity.this.updateHandler.sendEmptyMessage(0);
}
}
and Class Game who every 50ms do
public void update() {
x += 1;
y += 2;
}
And on draw method in this class
protected void onDraw(Canvas canvas) {
Plocha pozadie = new Plocha(paint, canvas, X0, Y0, vzdialenost);
if (nehra==true) {
paint.setStrokeWidth(13F);
paint.setStyle(Paint.Style.STROKE);
LevelUp(level);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setStyle(Paint.Style.FILL);
pozadie.vypis(paint, canvas, "Score", 100, 100, 6);
pozadie.vypis(paint, canvas, Integer.toString(zasah), 90, 200, 9);
pozadie.vypis(paint, canvas, "Level", 850, 100, 6);
pozadie.vypis(paint, canvas, Integer.toString(level), 850, 200, 8);
pozadie.vypis(paint, canvas, "Clicks", 1080 / 2 - 100, 100, 7);
pozadie.vypis(paint, canvas, Integer.toString(click), 1080 / 2, 200, 9);
}
everything be okay (y) but only one type display.
If use
Bitmap b = Bitmap.createBitmap(1080,
1920,Bitmap.Config.ARGB_8888);
this.canvas = new Canvas(b);
this.canvas.drawColor(Color.WHITE);
this.b=b;
and rewrite some parts of code
application result is these canvas
protected void onDraw( Canvas canvas ) {
I don't know why...
ImageView im=(ImageView)findViewById(R.id.imageView222);
game = new Game(this,im);
setContentView(game);
I think so If I save setContentView(game); to imageview. will be everything okay. But I try and every ideas finish more errors.
I'm sad from...
Thanks from some ideas.
I am trying to create some animated game play text for my breakout game in Java Swing GUI.
Expected behavior: Everytime a brick is hit its points will Slam onto the screen, pause for 0.25 seconds and then fade up into nothing.
What I have done: A timer is used inside a method inside a class called AlertText. When the brick is hit in the class GameLogic, a new AlertText is created and its timers start running. Now in the Game Class I have the paint class.
Question: So how do I call upon the specific instances of AlertText that were created in GameLogic to use the setter and getter methods to set my g.drawString in paint in Game class. I feel like this should be a common technique? Is there a name for it?
I got it to work with Global variables for one style of brick so I know animation is working, but I would need 100+ global variables to do every kind of brick.
Game Class
public class Game extends JPanel
{
public static final int HEIGHT = 720;
public static final int WIDTH = 600;
public Color color;
private GameLogic gl = new GameLogic();
private KeyboardController controller;
public Paddle player = new Paddle(110, HEIGHT-30, 100, 10, 10, color.black, controller);
public Ball gameBall = new Ball(300, 300, 15, color.black);
private boolean PaddleUpdateComplete = false;
private List<AlertText> activeAlerts = new ArrayList<AlertText>();
Game game = new Game();
public void spawnNewAlert(Brick b){
AlertText alert = new AlertText();
alert.setxPos(b.getXPosition());
alert.setyPos(b.getYPosition());
alert.setText(b.getPoints()+"");
alert.setColor(b.getColor());
activeAlerts.add(alert);
alert.fireText();
}
#Override
public void paint(Graphics g)
{
g.clearRect(0, 0, WIDTH, HEIGHT);
g.setColor(Color.WHITE);
g.fillRect(0, 0, WIDTH, HEIGHT);
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, (int)AlertText.staticAlertSize));
g.setColor(new Color(AlertText.staticRed, AlertText.staticGreen, AlertText.staticBlue));
g.drawString(AlertText.staticAlertOne, AlertText.staticAlertXPos, AlertText.staticAlertYPos);
player.draw((Graphics2D)g);
gameBall.draw((Graphics2D)g);
gl.drawBricks(g);
// Draw GameObjects and anything else here
g.setFont(scoreFont);
g.drawString("Score: " + player.getScore(), 10, 25);
g.drawString("LIVES: " + player.getLives(), 150, 25);
if(gl.gameOver(player) &&
gameBall.getYPosition() >= HEIGHT){
g.setColor(Color.black);
g.setFont(endFont);
g.drawString("Game Over! Score: " + player.getScore(), (WIDTH/2) - 85, (HEIGHT/2));
}
if(gl.empty()){
g.setColor(Color.black);
g.setFont(endFont);
g.drawString("You won! Score: " + player.getScore(), (WIDTH/2) - 85, (HEIGHT/2));
timer.stop();
}
if(PowerUps.isMegaPaddle){
g.setColor(Color.orange);
g.setFont(TimeFont);
g.drawString(PowerUps.megaPaddlecount+"", 300, 500);
}
if(PowerUps.isMegaBall){
g.setColor(Color.red);
g.setFont(TimeFont);
g.drawString(PowerUps.megaBallcount+"", 250, 400);
}
if(!game.activeAlerts.isEmpty()){
for(AlertText alert: game.activeAlerts){
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, alert.getTextSize()));
g.setColor(alert.getColor());
g.drawString(alert.getText(), alert.getxPos(), alert.getxPos());
if(alert.count<=0){
game.activeAlerts.remove(alert);
}
}
}
}
public void updateGameState()
{
gameBall.move();
player.move(controller);
gl.checkCollisions(gameBall, player, timer, WIDTH, HEIGHT, game);
gl.removeBrick();
// Move GameObjects and check for collisions here
if(Paddle.paddleHits==1 && !PaddleUpdateComplete){
gameBall.setXVelocity(10);
gameBall.setYVelocity(gameBall.getYVelocity()-6);
PaddleUpdateComplete = true;
}
}
public final void setupGame()
{
gameBall.setXVelocity(0);
gameBall.setYVelocity(-10);
player.setLives(5);
gl.makeBricks();
// Instantiate instance variables here
}
// Constructor method should not be modified
public Game()
{
// Set the size of the Panel to the correct size
this.setPreferredSize(new Dimension(WIDTH, HEIGHT));
// Set the background color of the Panel to black
this.setBackground(Color.BLACK);
// Instantiate a KeyboardController and listen for input with it
controller = new KeyboardController();
this.addKeyListener(controller);
// Call the setupGame method to initialize instance variables
this.setupGame();
// Get focus in the window
this.setFocusable(true);
this.requestFocusInWindow();
}
// Start method should not be modified
public void start()
{
// Set up a new Timer to repeat every 20 milliseconds (50 FPS)
timer = new Timer(20, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e)
{
repaint();
updateGameState();
}
});
timer.setRepeats(true);
timer.start();
}
Timer timer;
}
Alert Class method fireText()
public void fireText(){
count = 50;
textSize=0;
Firing=true;
Timer time = new Timer(50, null);
time.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
if(count>47){
textSize+=10;
xPos-=2;
count--;
}
else if(count>42){
count--;
}
else {
yPos -= 1;
xPos += 1;
textSize -= 1;
count--;
if(count<=0) {
text ="";
count=-1;
Firing =false;
time.stop();
}
}
}
});
time.start();
}
GameLogic method
public void checkCollisions(Ball ball, Paddle player, Timer time, int WIDTH, int HEIGHT, Game game) {
if(hitPaddle(ball, player)){
ball.setYVelocity(ball.getYVelocity() * -1);
Paddle.paddleHits++;
return;
}
//check if ball hit any walls
if(ball.getXPosition() >= (WIDTH - ball.getDiameter()) || ball.getXPosition() <= 0){
ball.setXVelocity(ball.getXVelocity() * -1);
}
if(ball.getYPosition() > (player.getYPosition() + player.getHeight() + 10)){
resetBall(ball, player, time, WIDTH, HEIGHT);
}
if(ball.getYPosition() <= 0){
ball.setYVelocity(ball.getYVelocity() * -1);
}
//handle collisions between bricks
int brickRowsActive = 0;
for(ArrayList<Brick> alb : bricks){
if(alb.size() == horizontalCount){
++brickRowsActive;
}
}
for(int i = (brickRowsActive==0) ? 0 : (brickRowsActive - 1); i < bricks.size(); ++i){
for(Brick b : bricks.get(i)){
if(brickHitByBall(ball, b)){
checkPowerUps(b, player, ball);
game.spawnNewAlert(b);
player.setScore(player.getScore() + b.getPoints());
b.decrementType();
}
}
}
}
Probably you want a list of alerts in your Game class where the rendering is done, and you want to be able to add to that list in your GameLogic class where you handle all the gameplay.
There are a number of ways to do this. Assuming you have a reference to your Game class in your GameLogic then move the code for spawnNewAlert() into Game. Then your code in GameLogic can call game.spawnNewAlert(b) and then leave it to the Game class to manage.
You will need to add a few things to your Game class:
a new member field private List<AlertText> activeAlerts = new ArrayList<AlertText>();
in spawnNewAlert() just before you fireText(), add the alert to activeAlerts
in paint(), loop through activeAlerts and draw each one, remove any that are no longer valid (be careful about how you remove, either use an Iterator or defer the removes to after the iteration to prevent a ConcurrentModificationException.)
As far as I understand you want to trigger a method in one object by another object both being wrapped up in the third, upper level one, right?
I would create both AlertText and GameLogic objects in the Game object and then pass the reference of the AlertText to the GameLogic, thus making it possible to the GameLogic to trigger the AletText's fireText() method. You would have to remove the instantiation of the AlertText from the spawnNewAlert method (in fact you need only one instance of AlertText), and rework a little bit the fireText method to reset after every run.
In GameLogic:
AlertText alert;
public GameLogic(AlertText alert //other parameters) {
this.alert = alert;
//other stuff you do here
}
In Game let's say:
GameLogic gameLogic;
AlertText alertText;
public Game() {
alertText = new AlertText();
gameLogic = new GameLogic(alertText);
}
public void paint(Graphics g) {
gameLogic.spawnNewAlert(brick);
}
I'm creating a graphical front-end for a JBox2D simulation. The simulation runs incrementally, and in between the updates, the contents of the simulation are supposed to be drawn. Similar to a game except without input.
I only need geometric primitives to draw a JBox2D simulation. This API seemed like the simplest choice, but its design is a bit confusing.
Currently I have one class called Window extending JFrame, that contains as a member another class called Renderer. The Window class only initializes itself and provides an updateDisplay() method (that is called by the main loop), that calls updateDisplay(objects) method on the Renderer. I made these two methods myself and their only purpose is to call repaint() on the Renderer.
Is the JPanel supposed to be used that way? Or am I supposed to use some more sophisticated method for animation (such that involves events and/or time intervals in some back-end thread)?
If you are wanting to schedule the updates at a set interval, javax.swing.Timer provides a Swing-integrated service for it. Timer runs its task on the EDT periodically, without having an explicit loop. (An explicit loop would block the EDT from processing events, which would freeze the UI. I explained this more in-depth here.)
Ultimately doing any kind of painting in Swing you'll still be doing two things:
Overriding paintComponent to do your drawing.
Calling repaint as-needed to request that your drawing be made visible. (Swing normally only repaints when it's needed, for example when some other program's window passes over top of a Swing component.)
If you're doing those two things you're probably doing it right. Swing doesn't really have a high-level API for animation. It's designed primarily with drawing GUI components in mind. It can certainly do some good stuff, but you will have to write a component mostly from scratch, like you're doing.
Painting in AWT and Swing covers some of the 'behind the scenes' stuff if you do not have it bookmarked.
You might look in to JavaFX. I don't know that much about it personally, but it's supposed to be more geared towards animation.
As somewhat of an optimization, one thing that can be done is to paint on a separate image and then paint the image on to the panel in paintComponent. This is especially useful if the painting is long: repaints can be scheduled by the system so this keeps when it happens more under control.
If you aren't drawing to an image, then you'd need to build a model with objects, and paint all of them every time inside paintComponent.
Here's an example of drawing to an image:
import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
/**
* Holding left-click draws, and
* right-clicking cycles the color.
*/
class PaintAnyTime {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new PaintAnyTime();
}
});
}
Color[] colors = {Color.red, Color.blue, Color.black};
int currentColor = 0;
BufferedImage img = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
Graphics2D imgG2 = img.createGraphics();
JFrame frame = new JFrame("Paint Any Time");
JPanel panel = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// Creating a copy of the Graphics
// so any reconfiguration we do on
// it doesn't interfere with what
// Swing is doing.
Graphics2D g2 = (Graphics2D) g.create();
// Drawing the image.
int w = img.getWidth();
int h = img.getHeight();
g2.drawImage(img, 0, 0, w, h, null);
// Drawing a swatch.
Color color = colors[currentColor];
g2.setColor(color);
g2.fillRect(0, 0, 16, 16);
g2.setColor(Color.black);
g2.drawRect(-1, -1, 17, 17);
// At the end, we dispose the
// Graphics copy we've created
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(img.getWidth(), img.getHeight());
}
};
MouseAdapter drawer = new MouseAdapter() {
boolean rButtonDown;
Point prev;
#Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
prev = e.getPoint();
}
if (SwingUtilities.isRightMouseButton(e) && !rButtonDown) {
// (This just behaves a little better
// than using the mouseClicked event.)
rButtonDown = true;
currentColor = (currentColor + 1) % colors.length;
panel.repaint();
}
}
#Override
public void mouseDragged(MouseEvent e) {
if (prev != null) {
Point next = e.getPoint();
Color color = colors[currentColor];
// We can safely paint to the
// image any time we want to.
imgG2.setColor(color);
imgG2.drawLine(prev.x, prev.y, next.x, next.y);
// We just need to repaint the
// panel to make sure the
// changes are visible
// immediately.
panel.repaint();
prev = next;
}
}
#Override
public void mouseReleased(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
prev = null;
}
if (SwingUtilities.isRightMouseButton(e)) {
rButtonDown = false;
}
}
};
PaintAnyTime() {
// RenderingHints let you specify
// options such as antialiasing.
imgG2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
imgG2.setStroke(new BasicStroke(3));
//
panel.setBackground(Color.white);
panel.addMouseListener(drawer);
panel.addMouseMotionListener(drawer);
Cursor cursor =
Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
panel.setCursor(cursor);
frame.setContentPane(panel);
frame.pack();
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
If the routine is long-running and repaints could happen concurrently, double buffering can also be used. Drawing is done to an image which is separate from the one being shown. Then, when the drawing routine is done, the image references are swapped so the update is seamless.
You should typically use double buffering for a game, for example. Double buffering prevents the image from being shown in a partial state. This could happen if, for example, you were using a background thread for the game loop (instead of a Timer) and a repaint happened the game was doing the painting. Without double buffering, this kind of situation would result in flickering or tearing.
Swing components are double buffered by default, so if all of your drawing is happening on the EDT you don't need to write double buffering logic yourself. Swing already does it.
Here is a somewhat more complicated example which shows a long-running task and a buffer swap:
import java.awt.*;
import javax.swing.*;
import java.awt.image.*;
import java.awt.event.*;
import java.util.*;
/**
* Left-click to spawn a new background
* painting task.
*/
class DoubleBuffer {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new DoubleBuffer();
}
});
}
final int width = 640;
final int height = 480;
BufferedImage createCompatibleImage() {
GraphicsConfiguration gc =
GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration();
// createCompatibleImage creates an image that is
// optimized for the display device.
// See http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsConfiguration.html#createCompatibleImage-int-int-int-
return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
}
// The front image is the one which is
// displayed in the panel.
BufferedImage front = createCompatibleImage();
// The back image is the one that gets
// painted to.
BufferedImage back = createCompatibleImage();
boolean isPainting = false;
final JFrame frame = new JFrame("Double Buffer");
final JPanel panel = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// Scaling the image to fit the panel.
Dimension actualSize = getSize();
int w = actualSize.width;
int h = actualSize.height;
g.drawImage(front, 0, 0, w, h, null);
}
};
final MouseAdapter onClick = new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
if (!isPainting) {
isPainting = true;
new PaintTask(e.getPoint()).execute();
}
}
};
DoubleBuffer() {
panel.setPreferredSize(new Dimension(width, height));
panel.setBackground(Color.WHITE);
panel.addMouseListener(onClick);
frame.setContentPane(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
void swap() {
BufferedImage temp = front;
front = back;
back = temp;
}
class PaintTask extends SwingWorker<Void, Void> {
final Point pt;
PaintTask(Point pt) {
this.pt = pt;
}
#Override
public Void doInBackground() {
Random rand = new Random();
synchronized(DoubleBuffer.this) {
Graphics2D g2 = back.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
g2.setBackground(new Color(0, true));
g2.clearRect(0, 0, width, height);
// (This computes pow(2, rand.nextInt(3) + 7).)
int depth = 1 << ( rand.nextInt(3) + 7 );
float hue = rand.nextInt(depth);
int radius = 1;
int c;
// This loop just draws concentric circles,
// starting from the inside and extending
// outwards until it hits the outside of
// the image.
do {
int rgb = Color.HSBtoRGB(hue / depth, 1, 1);
g2.setColor(new Color(rgb));
int x = pt.x - radius;
int y = pt.y - radius;
int d = radius * 2;
g2.drawOval(x, y, d, d);
++radius;
++hue;
c = (int) (radius * Math.cos(Math.PI / 4));
} while (
(0 <= pt.x - c) || (pt.x + c < width)
|| (0 <= pt.y - c) || (pt.y + c < height)
);
g2.dispose();
back.flush();
return (Void) null;
}
}
#Override
public void done() {
// done() is completed on the EDT,
// so for this small program, this
// is the only place where synchronization
// is necessary.
// paintComponent will see the swap
// happen the next time it is called.
synchronized(DoubleBuffer.this) {
swap();
}
isPainting = false;
panel.repaint();
}
}
}
The painting routine is just intended draw garbage which takes a long time:
For a tightly coupled simulation, javax.swing.Timer is a good choice. Let the timer's listener invoke your implementation of paintComponent(), as shown here and in the example cited here.
For a loosely coupled simulation, let the model evolve in the background thread of a SwingWorker, as shown here. Invoke publish() when apropos to you simulation.
The choice is dictated in part by the nature of the simulation and the duty cycle of the model.
Why not just use stuff from the testbed? It already does everything. Just take the JPanel, controller, and debug draw. It uses Java 2D drawing.
See here for the JPanel that does the buffered rendering:
https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/TestPanelJ2D.java
and here for the debug draw:
https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/DebugDrawJ2D.java
See the TestbedMain.java file to see how the normal testbed is launched, and rip out what you don't need :)
Edits:
Disclaimer: I maintain jbox2d
Here is the package for the testbed framework: https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework
TestbedMain.java is in the j2d folder, here:
https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d