i am trying to do a roulette casino game, so for this i made my roulette using the Arc2D package.
My code below
package roulette;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Arc2D;
import java.awt.geom.AffineTransform;
import javax.swing.Timer;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class RouletteInterface extends JPanel{
public int spinValue = 0;
public void paint(Graphics g){
Graphics2D g2d = (Graphics2D)g;
paintRoulette(g2d);
}
public void paintRoulette(Graphics2D g2d) {
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHints(hints);
AffineTransform at = AffineTransform.getTranslateInstance(10, 10);
at.rotate(spinValue, 10, 10);
double angle = 360 / 36.9;
double startAngle = 0;
int color = 0;
for(int i = 0; i < 37; i++) {
if(i == 0) {
g2d.setColor(Color.GREEN);
} else {
if(color == 0) {
g2d.setColor(Color.BLACK);
color = 1;
} else {
g2d.setColor(Color.RED);
color = 0;
}
}
g2d.fill(new Arc2D.Double(100, 100, 300, 300, startAngle, angle, Arc2D.PIE));
startAngle += angle;
}
g2d.transform(at);
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
spinValue += 0.01;
repaint();
}
});
timer.start();
}
}
In short i am not using a generalpath because i want to fill each arc with color red/green or black like the original roulette, and for the rotation i tried using a timer to increase the spinValue (this worked for me but when i use a generalpath) for the AfinneTransformation, but when i run the code, well, nothing happens. It shows me only the roulette without animation. What can i do?
Thanks in advance.
Painting and graphics in general are quite advanced topics, Java/Swing does a good job to "commonalise" the APIs into something which is reasonable easy to use, but still takes time and effort to learn and understand fully.
I would highly recommend having Performing Custom Painting, Painting in AWT and Swing and 2D Graphics and the JavaDocs booked marked, as you will be coming back to them on a regular bases (I still do)
There are lots of issues, which are compounding to make your life difficult.
Starting with...
public void paint(Graphics g){
Graphics2D g2d = (Graphics2D)g;
paintRoulette(g2d);
}
You should favour overriding paintComponent instead of paint, paint is a complicated process and you need to choose your entry point into carefully. Also, you should always call the paint methods super method, unless you are absolutely, positively prepared to take over its core functionality yourself.
In your case, you should also be making a copy of the Graphics context before passing it to paintRoulette, as Graphics is a shared resource and the transformations you are applying will cause issues for anything which is painted after your component.
Transformations...
AffineTransform at = AffineTransform.getTranslateInstance(10, 10);
at.rotate(spinValue, 10, 10);
This is somewhat interesting. You're creating translation of 10x10 which will move the origin point of the Graphics context. You then apply a rotation, which is anchored to 10x10.
The reason I mention it is because you then do...
g2d.fill(new Arc2D.Double(100, 100, 300, 300, startAngle, angle, Arc2D.PIE));
This means that the arc is offset by 110x110 from the corner of the component (add in your translation) and you'll be rotating about a point 20x20 from the component's top/left corner (add in your translation) ... this is weird to me because the centre of the of wheel is actually at 250x250 (from the component's top/left corner) which is going to make for one very weird affect.
Finally, you apply the transformation AFTER the painting is done AND then create a Timer inside the paint method...
Painting is done in serial. So one operation will effect the next, this will mean you will need to apply the transformation BEFORE you paint something (that you want transformed)
You also need to understand that you don't control the paint process, this means that your component may be painted for any number of reason at any time without your interaction. This means you could an infinite number of Timers, over a very small period of time.
Instead, your timer should be controlled externally from the paint process.
One other thing that took me some time to work out is...
public int spinValue = 0;
//...
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
spinValue += 0.01;
repaint();
}
});
You declare spinValue as int, but are adding a floating point value to it, this will have the effect of the decimal component been truncated, so the value will ALWAYS be 0.
Also, AffineTransform#rotate expects angles to be in radians, not degrees. Not sure if it's important, but you should be aware of it.
Runnable example...
Okay, so after applying the above, the code "might" look something like...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new RoulettePane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class RoulettePane extends JPanel {
private double spinValue = 0;
private Timer timer;
public RoulettePane() {
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
spin();
}
});
}
#Override
public Dimension getPreferredSize() {
return new Dimension(300, 300);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
paintRoulette(g2d);
g2d.dispose();
}
protected void spin() {
if (timer != null && timer.isRunning()) {
return;
}
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
spinValue += 0.01;
repaint();
}
});
timer.start();
}
protected void paintRoulette(Graphics2D g2d) {
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHints(hints);
int width = getWidth();
int height = getHeight();
int dimeter = Math.min(width, height);
AffineTransform at = AffineTransform.getRotateInstance(spinValue, dimeter / 2, dimeter / 2);
g2d.transform(at);
double angle = 360 / 36.9;
double startAngle = 0;
int color = 0;
for (int i = 0; i < 37; i++) {
if (i == 0) {
g2d.setColor(Color.GREEN);
} else {
if (color == 0) {
g2d.setColor(Color.BLACK);
color = 1;
} else {
g2d.setColor(Color.RED);
color = 0;
}
}
g2d.fill(new Arc2D.Double(0, 0, dimeter, dimeter, startAngle, angle, Arc2D.PIE));
startAngle += angle;
}
}
}
}
nb: I took the translation out for the time been as I wanted to focus on making the output more dynamic based on the actual width/height of the component
Related
I'm making a snake game in Java.
To make it efficient, I only paint the positions that have changed this frame (the first and last cells of the snake).
Here's my code:
public class GameCanvas extends JPanel {
private final List<GameChange> changes;
public GameCanvas() {
setBackground(Color.darkGray);
changes = new ArrayList<>();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
for (GameChange change : changes) {
Vector2 pos = change.position;
int val = change.value;
if (val != 0) g2d.setColor(Color.BLACK);
else g2d.setColor(Color.WHITE); //getBackground();
g2d.fillRect(GameWindow.pixelSize * pos.x,GameWindow.pixelSize * pos.y, GameWindow.pixelSize, GameWindow.pixelSize);
}
changes.clear();
}
public void applyChanges(List<GameChange> changes) {
this.changes.addAll(changes);
repaint();
}
}
The only problem is that the paintComponent method is repainting the background and the middle of the snake is disappearing.
The black cell is the new head and the white one is the one that I'm deleting. The middle cell was supposed to be black.
EDIT:
Everyone is telling me to draw the whole map but I really want to work on performance. This is what I've done so far:
private void paintChanges(List<GameChange> changes) {
Graphics2D g2d = (Graphics2D) getGraphics();
// Here I paint only the changes
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
// Here I paint everything
}
When the snake moves I only paint the changes but if the the frame gets repainted automatically the snake wont disappear. It appears to be working but I'm not sure if it's safe to use the getGraphics() method.
This is obviously not your game but it may help dispel the notion that you need to do something special to increase painting efficiency.
To create the snake, hold the left button down and drag out a curved line.
then release the button and just move the mouse around in the panel and watch the "snake" follow the mouse.
you can add more points by dragging the mouse at any time.
This works by simply creating a list of points and drawing a line in between them. As the mouse moves it removes the first point and adds the current one to the end. The paint method just iterates thru the list for each mouse movement and draws all the lines, giving the appearance of movement.
There is one obvious (and perhaps other, not so obvious) flaws with this. The number of points is constant but the snake expands and contracts in size since the points are spread out as the mouse moves faster so the lines between the points are longer. But that is not related to painting but to the speed of the mouse movement.
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class SimpleSnake extends JPanel {
JFrame frame = new JFrame("Simple Snake");
List<Point> snake = new ArrayList<>();
final static int WIDTH = 500;
final static int HEIGHT = 500;
public static void main(String[] args) {
SwingUtilities.invokeLater(()-> new SimpleSnake());
}
public SimpleSnake() {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
addMouseMotionListener(new MyMouseListener());
setBackground(Color.white);
frame.add(this);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(WIDTH, HEIGHT);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
if (snake.size() < 2) {
return;
}
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.black);
g2d.setStroke(new BasicStroke(3)); // line thickness
Point start = snake.get(0);
for(int i = 1; i < snake.size(); i++) {
Point next = snake.get(i);
g2d.drawLine(start.x, start.y, next.x, next.y);
start = next;
}
g2d.dispose();
}
public class MyMouseListener extends MouseAdapter {
public void mouseDragged(MouseEvent me) {
snake.add(new Point(me.getX(), me.getY()));
repaint();
}
public void mouseMoved(MouseEvent me) {
if(snake.isEmpty()) {
return;
}
snake.remove(0);
snake.add(new Point(me.getX(), me.getY()));
repaint();
}
}
}
as part of a school project we have to create a little game using Applets. I'm working on some tests right now but there's one thing I can't quite figure out:
I want to have multiple objects flying on my screen at the same time on my Applet screen. The animation effect is created by drawing the object, deleting it then moving it after a while.
Here's my code:
Robotworld class
package core;
import items.Obstacle;
import java.applet.Applet;
import java.awt.*;
import java.util.ArrayList;
public class Roboterwelt extends Applet {
private ArrayList<Obstacle> obstacles = new ArrayList<>();
#Override
public void init() {
setSize(600, 600);
Graphics g = getGraphics();
g.setColor(Color.BLACK);
for(int x = 0; x < 5; x++) {
Obstacle h = new Obstacle((x+1)*100, 100, g, this);
obstacles.add(h);
Thread t = new Thread(h);
t.start();
}
}
#Override
public void paint(Graphics g) {
for(Obstacle o : obstacles) {
o.draw();
}
}
}
Obstacle class
package items;
import java.applet.Applet;
import java.awt.*;
public class Obstacle implements Runnable {
private int x;
private int y;
private Graphics g;
public Hindernis(int x, int y, Graphics g) {
this.x = x;
this.y = y;
this.g = g;
}
public void draw() {
g.drawOval(x, y, 50, 50); //Draw obstacle
}
//Deleting the obstacle by covering it with a white circle
public void delete() {
g.setColor(Color.WHITE); //Change the color to white
g.fillOval(x-5,y-5,60,60); //Making it a bit bigger than the obstacle to fully cover it
g.setColor(Color.BLACK); //Reset the color to black
}
#Override
public void run() {
try {
while(y < 600) {
delete();
y += 10;
draw();
Thread.sleep(1000);
}
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
The problem is the part where I change the color of the Graphics object to cover the circle in white. When I have multiple threads running to represent the multiple obstacles on my screen and redrawing AND deleting happens concurrently, a thread gets interrupted after changing the color to white and draws a filled oval with the Graphics object which color was set to black by another thread that ran the delete() method to the end.
How can I force the program to not interrupt the delete() method between the color change to white and the drawing of the filled oval shape?
Disclaimer
Applet is deprecated, it is no longer supported by browsers, Oracle or the community. It would be unprofessional of me to try and encourage you to keep using them.
I appreciate that this is a "school" assignment, but perhaps it's time your instructor caught up with the rest of the world and started using something which doesn't actual cause more issues then it solves (hint JavaFX) - IMHO
Answer...
Don't use getGraphics, this is not how custom painting should be done. Painting should be done within the confines of the paint methods. Take a look at Painting in AWT and Swing for details. Apart from solving your immediate issue, your current approach risks been "wiped" clean when the applet repaints itself.
Overriding paint of the top level containers like Applet is a bad idea. Apart from locking you into a single use case, they aren't double buffered, which will cause flickering when painting occurs. The simplest solution is to start with a JPanel, which is double buffered and which can be added to what ever container you want to use.
You don't need multiple threads. Thread is a bit of an art form. More threads doesn't always mean more work gets done and can actually degrade the performance of the system. In your case you want to "update" the state in a single pass and then schedule a paint pass, so that the operations are synchronised in a single step and you don't end up with "dirty" updates
The following example simple makes use of Swing, which is based on AWT. It uses a JFrame instead of an Applet, but the concept is easily transferable, because the core functionality is based on a JPanel, so you can add it to what ever you want.
It makes use of a Swing Timer, which basically schedules a callback on a regular bases, but does it in away which makes it safe to update the state of the UI from (this replaces your Thread).
By using paintComponent to paint the Obstacles, we get two things for free.
Double buffering, so no more flickering
The Graphics context is automatically prepared for us, we don't need to "delete" the objects first, we simply paint the current state
The example also removes the Obstacle once it passes the edge of the panel, so you don't waste time trying to move/paint it when it's no longer visible.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
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("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private List<Obstacle> obstacles;
public TestPane() {
Color[] colors = new Color[]{Color.RED, Color.GREEN, Color.BLUE, Color.MAGENTA, Color.YELLOW};
obstacles = new ArrayList<>(10);
int y = 0;
for (int index = 0; index < 5; index++) {
y += 55;
Obstacle obstacle = new Obstacle(y, 0, colors[index]);
obstacles.add(obstacle);
}
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
Iterator<Obstacle> it = obstacles.iterator();
while (it.hasNext()) {
Obstacle ob = it.next();
if (ob.move(getSize())) {
it.remove();
}
}
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
Iterator<Obstacle> it = obstacles.iterator();
while (it.hasNext()) {
Obstacle ob = it.next();
ob.paint(g2d);
}
g2d.dispose();
}
}
public class Obstacle {
private int x, y;
private Color color;
public Obstacle(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
public void paint(Graphics2D g2d) {
g2d.setColor(color);
g2d.fillRect(x, y, 50, 50);
}
public boolean move(Dimension size) {
y += 1;
return y > size.height;
}
}
}
But all the Obstacles move at the same rate!
Yeah, that's because you used a single delta. If you want the Obstacles to move at different rates, then change the deltas, for example...
public static class Obstacle {
private static Random RND = new Random();
private int x, y;
private Color color;
private int yDelta;
public Obstacle(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
yDelta = RND.nextInt(5) + 1;
}
public void paint(Graphics2D g2d) {
g2d.setColor(color);
g2d.fillRect(x, y, 50, 50);
}
public boolean move(Dimension size) {
y += yDelta;
return y > size.height;
}
}
I am learning the basics of java games programming and I am confused about a few things.
I know you use the "canvas" class to create a blank canvas and then use the paint method to create stuff.
But what does Graphics2D? I have seen people using the grahpics2d class to create a canvas for example
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.BLACK);
Now why did they use the grahpics2d and not the canvas?
Also I have seen people creating shapes like a rectangle by using:
Rectangle r = new Rectangle();
but some people have created them like:
Shape shape = new Rectangle2D.Double(value1,valu2,valu3,valu4);
What's the difference between these two?
Thanks in advance.
regards,
First, no I wouldn't use a Canvas object but rather a JPanel, and I'd draw in the paintComponent method override, not the paint method. Think of the JPanel as if it were a paint canvas and the Graphics or Graphics2D as if it were the brush that you were using to paint with. So in other words, you would need them both to create your drawing.
As for Rectangle vs. Rectangle2D, the 2D shapes are part of a newer addition to Graphics, when Graphics2D came about. These are based on the Shape interface, one that allows you a little more flexibility and OOPs to your drawing.
For greater detail, please have a look at:
Lesson: Performing Custom Painting
Trail: 2D Graphics
Painting in AWT and Swing
Edit
Re your questions:
Q: So you would use JPanel as my empty canvas and the Graphics2D g2d = (Graphics2D) g; create a brush kind of thing that you can use to change the JPanel. Hence this g2d.setColor(Color.BLACK); changes the background colour of our JPanel canvas. Is this right?
Yes. And you can even change the Graphics2D object's Stroke via
g2d.setStroke(new BasicStroke(...));
Q: Also can you explain to me what is "Shape" and what do you use it for?
Please look at the 2nd tutorial that I've linked to above as it will go into a fair bit of detail on what Shape represents and how to use it. It is in sum an interface used by all of the Xxxxx2D classes such as Rectangle2D and Ellipse2D. And it allows them all to share certain properties including being fillable, drawable, transormable, and more.
Edit 2
For example:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import javax.swing.*;
#SuppressWarnings("serial")
public class RotateFoo extends JPanel {
private static final int PREF_WIDTH = 800;
private static final int PREF_HEIGHT = 600;
private static final Color STAR_COLOR = Color.red;
private static final int ROTATE_TIMER_DELAY = 20;
private static final int POINTS = 5;
private static final int RADIUS = 50;
private static final String TITLE = "Press \"r\" to rotate";
private static final float TITLE_POINTS = 52f;
private Path2D star = new Path2D.Double();
private Timer rotateTimer = new Timer(ROTATE_TIMER_DELAY, new RotateTimerListener());
public RotateFoo() {
double x = 0.0;
double y = 0.0;
double theta = 0.0;
for (int i = 0; i <= POINTS; i++) {
x = RADIUS + RADIUS * Math.cos(theta);
y = RADIUS + RADIUS * Math.sin(theta);
if (i == 0) {
star.moveTo(x, y);
} else {
star.lineTo(x, y);
}
theta += 4 * Math.PI / POINTS;
}
double tx = (getPreferredSize().getWidth() - star.getBounds().getWidth()) / 2;
double ty = (getPreferredSize().getHeight() - star.getBounds().getHeight()) / 2;
AffineTransform at = AffineTransform.getTranslateInstance(tx, ty);
star.transform(at );
int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;
InputMap inputMap = getInputMap(condition);
ActionMap actionMap = getActionMap();
String rotateOn = "rotate on";
String rotateOff = "rotate off";
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0, false), rotateOn);
actionMap.put(rotateOn, new AbstractAction() {
public void actionPerformed(ActionEvent arg0) {
if (rotateTimer != null && !rotateTimer.isRunning()) {
rotateTimer.start();
}
}
});
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0, true), rotateOff);
actionMap.put(rotateOff, new AbstractAction() {
public void actionPerformed(ActionEvent arg0) {
if (rotateTimer != null && rotateTimer.isRunning()) {
rotateTimer.stop();
}
}
});
//rotateTimer.start();
JLabel titleLabel = new JLabel(TITLE, SwingConstants.CENTER);
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, TITLE_POINTS));
add(titleLabel);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(PREF_WIDTH, PREF_HEIGHT);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(STAR_COLOR);
if (star != null) {
g2.draw(star);
}
}
private class RotateTimerListener implements ActionListener {
private static final double BASE_THETA = Math.PI / 90;
#Override
public void actionPerformed(ActionEvent e) {
double anchorx = getPreferredSize().getWidth() / 2;
double anchory = getPreferredSize().getHeight() / 2;
AffineTransform at = AffineTransform.getRotateInstance(BASE_THETA, anchorx, anchory);
star.transform(at);
repaint();
}
}
private static void createAndShowGui() {
RotateFoo mainPanel = new RotateFoo();
JFrame frame = new JFrame("RotateFoo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
you have to make a certain difference:
a Canvas (or maybe a Panel or a JPanel or a Frame) are Objects that represent an GUI-Object! such an object is required to capture Input Events, or maybe used to be layouted. it can be set active and disabled, all that stuff.
a Graphics Objects is that thing, that is inside of the canvas. it is responsible for the pure drawing! it can use special drawing features, having strokes and fonts and colors...
so - you have two different classes for different purpose! it took me a while to understand that...
excuse my puny english...
I am making a simple animation in Processing. I want to animate an image from its starting point to a defined x,y value on the screen.
I have 2 methods, update() and draw(), which are run on every tick. update() is where the code will go to process the x/y coordinates to provide to the draw() method on the next tick.
The draw() method then draws the image, passing in the updated x and y values.
minimal example:
class ScrollingNote {
float x;
float y;
float destX;
float destY;
PImage noteImg;
ScrollingNote(){
noteImg = loadImage("image-name.png");
this.x = width/2;
this.y = 100;
this.destX = 100;
this.destY = height;
}
void update(){
// TODO: adjust this.x and this.y
// to draw the image slightly closer to
// this.destX and this.destY on the redraw
// ie in this example we are animating from x,y to destX, destY
}
void draw(){
image( noteImg, this.x, this.y );
}
}
What sort of calculation do I need to make to adjust the x/y coordinates to make the image draw slightly closer to the destination?
This is a very basic example of time period based animation
It will animate a Animatable object over a 5 second period. You could simply use a List and update/paint multiple objects simultaneously if you wanted to get fancy.
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.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class AnimationTest {
public static void main(String[] args) {
new AnimationTest();
}
public AnimationTest() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public interface Animatable {
public void update(double progress);
public void draw(Graphics2D g2d);
}
public static class FlyingSquiral implements Animatable {
private final Point startPoint;
private final Point targetPoint;
private final double startAngel;
private final double targetAngel;
private Point location;
private double angle;
public FlyingSquiral(Point startPoint, Point targetPoint, double startAngel, double targetAngel) {
this.startPoint = startPoint;
this.targetPoint = targetPoint;
this.startAngel = startAngel;
this.targetAngel = targetAngel;
location = new Point(startPoint);
angle = startAngel;
}
#Override
public void update(double progress) {
location.x = (int)Math.round(startPoint.x + ((targetPoint.x - startPoint.x) * progress));
location.y = (int)Math.round(startPoint.y + ((targetPoint.y - startPoint.y) * progress));
angle = startAngel + ((targetAngel - startAngel) * progress);
}
#Override
public void draw(Graphics2D g2d) {
Graphics2D clone = (Graphics2D) g2d.create();
clone.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
clone.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
clone.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
clone.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
clone.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
clone.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
clone.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
clone.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
AffineTransform at = new AffineTransform();
at.translate(location.x, location.y);
at.rotate(Math.toRadians(angle), 25, 25);
clone.setTransform(at);
clone.draw(new Rectangle(0, 0, 50, 50));
clone.dispose();
}
}
public static class TestPane extends JPanel {
public static final long DURATION = 5000;
private long startTime;
private boolean started = false;
private FlyingSquiral squiral;
public TestPane() {
squiral = new FlyingSquiral(
new Point(0, 0),
new Point(150, 150),
0d, 360d);
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (!started) {
startTime = System.currentTimeMillis();
started = true;
}
long time = System.currentTimeMillis();
long duration = time - startTime;
if (duration > DURATION) {
duration = DURATION;
((Timer)e.getSource()).stop();
}
double progress = (double)duration / (double)DURATION;
squiral.update(progress);
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
squiral.draw(g2d);
g2d.dispose();
}
}
}
Equally, you could use a constraint based animation, where by the object keeps moving until it meets it's required constraints (angel/position). Each has pros and cons.
I prefer a time period based approach as it allows me to apply different transformations without needing to care about pre-calculating the delta. Try this, change the target angel from 360 to 720 and run it again.
I also prefer to use an animation library, as they add additional features, like interpolation, allowing to change the speed of the animation at certain points in time without changing the duration, this would allow you to do things like slow in, slow out (ramp up/out) effects, making the animation more appealing.
Take a look at...
Timing Framework
Trident
Unviersal Tween Engine
If you are using Processing 2.0 this can be done via Ani library. To get same output like #MadProgrammer you just setup basic sketch with Ani.init(this) then in draw() function move box via translate() and rotate it via rotate() functions. Whole animation begins after first mouse click.
import de.looksgood.ani.*;
import de.looksgood.ani.easing.*;
float posX = 25, posY = 25;
float angleRotation = 0;
void setup () {
size (200, 200);
background (99);
noFill ();
stroke (0);
Ani.init(this);
frameRate (30);
rectMode(CENTER);
}
void draw () {
background (225);
translate(posX, posY);
rotate(radians(angleRotation));
rect(0, 0, 50, 50);
}
void mousePressed() {
Ani.to(this, 5, "posX", 175, Ani.LINEAR);
Ani.to(this, 5, "posY", 175, Ani.LINEAR);
Ani.to(this, 5, "angleRotation", 360, Ani.LINEAR);
}
Manually you can get similar result just by increasing posX, posY and angleRotation within draw loop.
I have a simple applet that animates a rectangle along the x-axis of the canvas. The problem is that it flickers. I have tried to Google this problem, but I didn't come up with anything useful or anything that I understood.
I am relatively new to Java.
Thanks!
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.*;
import javax.swing.*;
public class simpleAnimation extends JApplet implements ActionListener {
Timer tm = new Timer(10, this);
int x = 0, velX = 2;
public void actionPerformed(ActionEvent event) {
if (x < 0 || x > 550){
velX = -velX;
}
x = x + velX;
repaint();
}
public void paint ( Graphics g ) {
super.paint(g);
g.setColor(Color.RED);
g.fillRect(x, 30, 50, 30);
tm.start();
}
}
**********UPDATED CODE WITHOUT FLICKER**********
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
public class simpleAnimation extends JApplet implements ActionListener
{
Graphics bufferGraphics;
Image offscreen;
Dimension dim;
int x = 3, velX = 2;
Timer tm = new Timer(10, this);
public void init()
{
dim = getSize();
offscreen = createImage(dim.width,dim.height);
bufferGraphics = offscreen.getGraphics();
}
public void paint(Graphics g)
{
bufferGraphics.clearRect(0,0,dim.width,dim.width);
bufferGraphics.setColor(Color.red);
bufferGraphics.fillRect(x,50,50,20);
g.drawImage(offscreen,0,0,this);
tm.start();
}
public void update(Graphics g)
{
paint(g);
}
public void actionPerformed(ActionEvent evt)
{
if ( x < 0 || x > 550){
velX = -velX;
}
x = x + velX;
repaint();
}
}
I used this applet as a template.
I always struggle with this concept of double buffering.
Here is my example that overrides paint() of the JApplet and a paintComponent() of a JPanel, which uses double buffering by default.
I don't see any difference in the apparent flickering.
//<applet code="SimpleAnimation.class" width="600" height="300"></applet>
import java.awt.*;
import java.awt.Graphics;
import java.awt.event.*;
import javax.swing.*;
public class SimpleAnimation extends JApplet implements ActionListener {
Timer tm = new Timer(10, this);
int x = 0, velX = 2;
JPanel panel;
public void init()
{
panel = new JPanel()
{
#Override
public Dimension getPreferredSize()
{
return new Dimension(50, 100);
}
#Override
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
g.setColor(Color.RED);
g.fillRect(x, 30, 50, 30);
}
};
add(panel, BorderLayout.SOUTH);
tm.start();
}
public void actionPerformed(ActionEvent event) {
if (x < 0 || x > 550){
velX = -velX;
}
x = x + velX;
repaint();
// panel.repaint();
}
public void paint ( Graphics g ) {
super.paint(g);
g.setColor(Color.RED);
g.fillRect(x, 30, 50, 30);
}
}
I test the code using: appletviewer SimpleAnimation.java
Is my concept of double buffering flawed, or my implementation, or both?
The problem is, top level containers like JApplet aren't double buffered. This means that when it's updated, the screen flickers as each action is done directly onto the screen.
Instead, you should create a custom component, using something like a JPanel, and override its paintComponent method and perform your custom painting actions there.
Because Swing components are double buffered the results of the paint action are buffered before they are painted to the screen, making it a single action
Take a look at Performing Custom Painting for more details
What you're doing now works like this:
public void paint ( Graphics g ) {
// draw the entire area white
super.paint(g);
g.setColor(Color.RED);
// draw a rectangle at the new position
g.fillRect(x, 30, 50, 30);
}
So with every step, you first wipe out your rectangle, and then draw it fresh. Thus the flickering - the pixels under the rectangle keep changing from white to red to white to red to white to red...
Now observe that the smallest amount of painting you need to do is (supposing rectangle moves to the right) this:
draw velx pixels on the left WHITE
draw velx pixes on the right RED
If you do that, your animation will be smooth.
Computing that can be quite challenging though, especially when your shape is more complicated than just a square / your movement is more complex. That's where double buffering comes in.
With double buffering, you create an in-memory image that is the same size as your screen. You paint your entire theme there. Then you paint that image on your screen all at once.
When doing that, there won't be an intermediate step of "entire screen is WHITE"; thus no flickering. But note that you end up re-painting the entire screen rather than just the area that changed, which isn't ideal. Consider using clipping - a technique where you repaint a pre-defined area of the image and not the entire thing.