Overlapping of paint GUI - java

So basically I'm trying to create countdown when my code is run,
which is fine and working the only problem I have is that instead of the
numbers disappearing and being replaced by the new value of i, they stay the new value overlaps the existing number. I tried to use repaint () to fix this but it's not working. I'm wondering if there's something I'm missing or if I should try a different way altogether.
here's the code for the timer:
public void timer (Graphics g,int x){
x=550;
for (int i = time;i>0;i--){
g.setColor(deepPlum);
g.setFont(new Font("Monospaced", Font.PLAIN, 25));
g.drawString("time: "+i, x, 650);
repaint();
}
}

At a "guess"
I don't like guessing, but it seems we're not going to get a runnable example which would provide a clear understand of how the problem is been generated.
Evidence...
I do call the super paint method but in the paint method. I know that is fine because all of my other graphics are working fine.
This "suggests" that timer is been called as part of the normal Swing "paint" cycle. If that's true, it explains the reason for the issue.
This...
public void timer (Graphics g,int x){
x=550;
for (int i = time;i>0;i--){
g.setColor(deepPlum);
g.setFont(new Font("Monospaced", Font.PLAIN, 25));
g.drawString("time: "+i, x, 650);
repaint();
}
}
is simply painting all the numbers over each other, each time a paint cycle occurs, which really isn't how a count down timer should be painted.
Instead, it should simply be painting the remaining time, a single value, and not try and paint all of them.
You can think of a "paint cycle" as a "frame", it paints a snap-shot of the current state of the component. You need to paint a number of these "frames" in order to animate the changes from one point in time to another.
I would, again, strongly, recommend that you take the time to read through Performing Custom Painting and Painting in AWT and Swing to better understand how painting actually works.
Without more evidence, I would suggest, a Swing Timer would be a better solution. This could be used to calculate the remaining time and schedule a repaint on a regular bases.
Because I seem to do this kind of thing a lot, I wrapped up the core functionality into a simple Counter class, which has a "duration" (or runtime) and can calculate the duration it's been running, the remaining time and the progress as a percentage of the duration and runTime, probably more then you need, but it gives you the basics from which to start.
Because painting is a imprecise art (the time between paint cycles is variable, even when using a Timer), I've used a "time" based approach, rather then a simple "counter". This gives you a much higher level of precision when dealing with these type of scenarios
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import javax.swing.JComponent;
import javax.swing.JFrame;
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());
// The following two lines just set up the
// "default" size of the frame
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JComponent {
private Counter counter;
public TestPane() {
Timer timer = new Timer(1000, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (counter == null) {
counter = new Counter(Duration.of(10, ChronoUnit.SECONDS));
counter.start();
}
repaint();
}
});
timer.start();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (counter != null) {
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
long time = counter.getRemaining().getSeconds();
int x = (getWidth() - fm.stringWidth(Long.toString(time))) / 2;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent();
g2d.drawString(Long.toString(time), x, y);
g2d.dispose();
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
public static class Counter {
private Duration length;
private LocalDateTime startedAt;
public Counter(Duration length) {
this.length = length;
}
public Duration getLength() {
return length;
}
public void start() {
startedAt = LocalDateTime.now();
}
public void stop() {
startedAt = null;
}
public Duration getDuration() {
if (startedAt == null) {
return null;
}
Duration duration = Duration.between(startedAt, LocalDateTime.now());
if (duration.compareTo(getLength()) > 0) {
duration = getLength();
}
return duration;
}
public Double getProgress() {
if (startedAt == null) {
return null;
}
Duration duration = getDuration();
return Math.min(1.0, (double) duration.toMillis() / length.toMillis());
}
public Duration getRemaining() {
if (startedAt == null) {
return null;
}
Duration length = getLength();
Duration time = getDuration();
LocalDateTime endTime = startedAt.plus(length);
LocalDateTime runTime = startedAt.plus(time);
Duration duration = Duration.between(runTime, endTime);
if (duration.isNegative()) {
duration = Duration.ZERO;
}
return duration;
}
}
}
Before someone tells me how clearRect would solve the issue, it's important to note that clearRect will do two things. One, it would only display the "last" value of the loop, which is clearly not what the OP wants, and it will fill the specified area with the current background color of the Graphics context, which "may" not be what the OP want's (as it will paint a nice rectangle over the top of what ever else was painted before it).
If used properly, the Swing painting system will prepare the Graphics context BEFORE the component's paint method is called, rendering the need for using clearRect mute

Call clearRect to restore the background for a rectangular region before drawing again

Related

Java Swing - Timer can't run faster than 15ms

I'm making a game in Java Swing. In it I have fairly simple graphics and not that many logic operations currently so I was confused why my frameTimes never went below 15ms, even I ran it at bare minimum.
Here's the declaration of one of my Timers as well as the bare minimum call I mentioned before.
logicTimer = new Timer(1, new TickTimer());
logicTimer.setInitialDelay(0);
...
class TickTimer implements ActionListener {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Logic processing took "+ (System.currentTimeMillis()-previousTime)+" ms");
tick();
previousTime = System.currentTimeMillis();
}
}
public void tick() {}
I have an essentially identical Timer for rendering as well. I render into a JPanel which is contained in a JFrame
For some additional background I'm having these results on a gaming laptop (Lenovo Y720) with a GTX 1060 MaxQ, an i7-7700HQ and 16GB RAM. I also have a similarly specced PC (1070, 7700K) that didn't have this issue if my memory serves right, but I can't test it right now. The laptop does have 60Hz screen, but that shouldn't matter as far as I know.
So I tried running my Timers with the bare minimum calls, but the frameTime was still either 15ms, 16ms, 30ms or 31ms, even though I set the delay to the minimum 1ms with no initial delay. I tried reducing the game's window size but that also had no effect. I tried unplugging my monitor that is connected to the laptop but the issue persisted. I tried closing any background windows and setting the JDK executable to high and realtime priorities in Windows to no avail.
You might say "15ms? THats still 60 fps, what is there to complain about?" but the problem is that I will have to present this game in a few months, likely on this laptop, and while the performance is just OK now, if I add complicated/lots of logic or visuals then a spike would be immediately noticable. This is assuming that this 15-30ms is a baseline and that if I had 1ms of processing that would result in 1ms higher base latency/frametime too.
After reading some previous questions regarding this issue lots of them came to the conclusion that this is an inherent and unfixable issue, but more modern threads talked about how this was solved with newer version of Swing where better precision Timers were used, so the source of the issue on modern hardware is the use a 32-bit JVM, but I'm fairly sure that I'm running 64-bit, as checked with System.getProperty("sun.arch.data.model"). Not sure if relevant but my JDK version is "17.0.2".
You might be running the application on Windows, where the default timer resolution is 15.6 ms.
JDK can temporarily increase the resolution of a system timer, when you call Thread.sleep with an interval not multiple to 10ms:
HighResolutionInterval(jlong ms) {
resolution = ms % 10L;
if (resolution != 0) {
MMRESULT result = timeBeginPeriod(1L);
}
}
Note: this is not a documented feature, and may be changed in any future version of JDK.
So the workaround is to start a background thread that calls Thread.sleep with an odd interval:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class HiResTimer {
public static void enableHiResTimer() {
Thread thread = new Thread("HiResTimer") {
#Override
public void run() {
while (true) {
try {
sleep(Integer.MAX_VALUE);
} catch (InterruptedException ignore) {
}
}
}
};
thread.setDaemon(true);
thread.start();
}
public static void main(String[] args) throws Exception {
enableHiResTimer();
new javax.swing.Timer(1, new ActionListener() {
long lastTime;
#Override
public void actionPerformed(ActionEvent e) {
System.out.println((System.nanoTime() - lastTime) / 1e6);
lastTime = System.nanoTime();
}
}).start();
Thread.currentThread().join();
}
}
However, this is a hack, and I'd suggest against doing so. The higher timer resolution increases CPU usage and power consumption.
Bare bones example, running at 1 millisecond
Top text is the time between timer ticks and the middle is the time between repaints.
This demonstrates that there is a 1 millisecond delay between repaints (it does bounce a bit, so it might 1-3 milliseconds). I prefer to use a 5 millisecond delay as there are overheads with the thread scheduling which might make it ... "difficult" to achieve 1 millisecond precision
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private Instant lastRepaintTick;
private Instant lastTick;
private JLabel label = new JLabel();
public TestPane() {
setLayout(new BorderLayout());
label.setHorizontalAlignment(JLabel.CENTER);
add(label, BorderLayout.NORTH);
Timer timer = new Timer(1, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (lastTick != null) {
label.setText(timeBetween(lastTick, Instant.now()));
}
repaint();
lastTick = Instant.now();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
protected String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (lastRepaintTick != null) {
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = (getHeight() - fm.getHeight()) / 2;
g2d.drawString(text, x, y + fm.getAscent());
g2d.dispose();
}
lastRepaintTick = Instant.now();
}
}
}
Note, this is not the "simplest" workflow, it's doing a lot work, creating a bunch of short lived objects, which, in of itself, could cause a drag on the frame rate. This leads me to think that your frame calculations are off.
I also tried...
System.out.println("Logic processing took " + (System.currentTimeMillis() - previousTime) + " ms");
if (lastTick != null) {
label.setText(timeBetween(lastTick, Instant.now()));
}
repaint();
lastTick = Instant.now();
previousTime = System.currentTimeMillis();
which prints 1-2 milliseconds. But please note System.out.println adds additional overhead and might slow down your updates
Java is going to be limited by the underlying hardware/software/driver/JVM implementation. Swing has been using DirectX or OpenGL pipelines for years (at least +5). If you're experiencing issues on one machine and not another, I'd be trying to explorer the differences between those two machines. I've had (lots) of issues with integrated video cards over the years and this tends to come down to the driver on the OS.
15ms is roughly 66fps, so it "could" be a delay in the rendering pipeline limited by the screen/driver/video hardware (guessing), but all my screens are running at 60hz and I don't have issues (I'm running 3 off my MacBook Pro), although I am running Java 16.0.2.
I've also been playing around with this example over the past few weeks. It's a simple concept of using "line boundary checking for hit detection". The idea was to use a "line of sight" projected through a field of animated objects to determine which ones could be "seen". The original question was suggesting that their algorithm has issues (frame drops) with < 100 entities.
The test I have has 15, 000 animated entities (and 18 fixed entities) and tends to run between 130-170fps (7.69-5.88 milliseconds) with a Timer set to a 5 millisecond interval. This example is NOT optimised and I found that drawing the "hits" was causing most of the slow down, but I don't honestly know exactly why.
My point been, that a relatively efficient model shouldn't see a large "degradation" of frame rate and a relatively efficient model should also be able to compensate for it.
If frame rate is really super important to you, then you're going to need to explore using a "active rendering" model (as apposed to the passive rendering model use by Swing), see BufferStrategy and BufferCapabilities for more details
I just tried your barebones 1ms example and its showing essentially the same results: 0s.01X where the X is 5, 6 or 7. This virtually guarantees that my problem doesn't have to do with my code, what other fixes can I try?
Well, nothing really. This seems to be a limitation of the hardware/drivers/OS/rendering pipeline.
The question, though, why is it so important? Generally speaking, 15ms is roughly 60fps. Why would you need more?
If you absolutely, without question, must have, the fastest rendering through put, then I'd suggest having a look at using a BufferStrategy. This takes you out of the overhead of the Swing rendering workflows, for example...
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics2D;
import java.awt.image.BufferStrategy;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
Frame frame = new Frame();
frame.setTitle("Make it so");
Canvas canvas = new Canvas() {
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
};
frame.add(canvas);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
canvas.createBufferStrategy(3);
Instant lastRepaintTick = null;
Instant lastTick = null;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSSS");
do {
BufferStrategy bs = canvas.getBufferStrategy();
while (bs == null) {
System.out.println("buffer");
bs = canvas.getBufferStrategy();
}
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
System.out.println("draw");
Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();
// Render to graphics
// ...
g2d.setColor(Color.LIGHT_GRAY);
g2d.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
if (lastRepaintTick != null) {
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (canvas.getWidth() - fm.stringWidth(text)) / 2;
int y = (canvas.getHeight() - fm.getHeight()) / 2;
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y + fm.getAscent());
text = LocalTime.now().format(formatter);
x = (canvas.getWidth() - fm.stringWidth(text)) / 2;
y += fm.getHeight();
g2d.drawString(text, x, y + fm.getAscent());
}
lastRepaintTick = Instant.now();
g2d.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (bs.contentsRestored());
System.out.println("show");
// Display the buffer
bs.show();
// Repeat the rendering if the drawing buffer was lost
} while (bs.contentsLost());
System.out.println("done");
try {
Thread.sleep(5);
} catch (InterruptedException ex) {
}
} while (true);
}
protected static String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
}
I did have this running without a loop, but in general, "wild loops" are a bad idea, as they tend to starve other threads.
BufferStrategy example does finally result in frametimes lower than 15ms, so I guess the limitation is somewhere in Timer on my hardware.
Just be careful with what's been displayed here. The BufferStrategy example is displaying the time between each LOOP cycle, not each frame. AFAIK there's no way to determine when a frame is actually painted on the screen (or if it is).
You could "try" doing a side by side comparison, but you might find it hard to tell the difference between the two, again, the time between the BufferStrategy rendering a page and presenting might have some drift, so the value "presented" by the BufferStrategy may be stale. Keep it in mind.
Canvas on top
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.setLayout(new GridLayout(2, 1));
TestCanvas testCanvas = new TestCanvas();
frame.add(testCanvas);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
testCanvas.start();
}
});
}
private static final long TIME_DELAY = 5;
private static final Dimension PREFERRED_SIZE = new Dimension(200, 35);
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.nnnnnnnnn");
protected String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
public class TestPane extends JPanel {
private Instant lastRepaintTick;
private Instant lastTick;
public TestPane() {
setLayout(new BorderLayout());
Timer timer = new Timer((int) TIME_DELAY, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
repaint();
lastTick = Instant.now();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return PREFERRED_SIZE;
}
protected String timeBetween(Instant then, Instant now) {
Duration duration = Duration.between(then, now);
String text = String.format("%ds.%03d", duration.toSecondsPart(), duration.toMillisPart());
return text;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (lastRepaintTick != null) {
Graphics2D g2d = (Graphics2D) g.create();
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = 0;
g2d.drawString(text, x, y + fm.getAscent());
text = LocalTime.now().format(TIME_FORMATTER);
x = (getWidth() - fm.stringWidth(text)) / 2;
y += fm.getHeight();
g2d.drawString(text, x, y + fm.getAscent());
g2d.dispose();
}
lastRepaintTick = Instant.now();
}
}
public class TestCanvas extends Canvas {
private Thread thread;
public TestCanvas() {
thread = new Thread(new Runnable() {
#Override
public void run() {
createBufferStrategy(3);
Instant lastRepaintTick = null;
do {
BufferStrategy bs = getBufferStrategy();
while (bs == null) {
bs = getBufferStrategy();
}
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics2D g2d = (Graphics2D) bs.getDrawGraphics();
// Render to graphics
// ...
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
if (lastRepaintTick != null) {
FontMetrics fm = g2d.getFontMetrics();
String text = timeBetween(lastRepaintTick, Instant.now());
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = 0;
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y + fm.getAscent());
text = LocalTime.now().format(TIME_FORMATTER);
x = (getWidth() - fm.stringWidth(text)) / 2;
y += fm.getHeight();
g2d.drawString(text, x, y + fm.getAscent());
}
lastRepaintTick = Instant.now();
g2d.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (bs.contentsRestored());
// Display the buffer
bs.show();
// Repeat the rendering if the drawing buffer was lost
} while (bs.contentsLost());
Instant tickTime = Instant.now();
try {
Thread.sleep(TIME_DELAY);
} catch (InterruptedException ex) {
}
} while (true);
}
});
}
public void start() {
thread.start();
}
#Override
public Dimension getPreferredSize() {
return PREFERRED_SIZE;
}
}
}

How to animate with java awt

I'm trying to make an animation of a red oval that will move to the right of the screen. But it just draws the oval. I don't know what I'm doing wrong and I literally can't find anything about how to do this. Any help would be awesome, thanks.
import java.awt.*;
public class mainClass
{
public mainClass()
{
Frame f = new Frame("Canvas Example");
f.add(new MyCanvas());
f.setLayout(null);
f.setSize(400, 400);
f.setVisible(true);
}
public static void main(String args[])
{
new mainClass();
}
}
class MyCanvas extends Canvas
{
int x = 75;
public MyCanvas() {
setBackground (Color.BLACK);
setSize(400, 400);
}
public void paint(Graphics g)
{
g.setColor(Color.red);
g.fillOval(x, 75, 150, 75);
}
public void update(Graphics g)
{
x++;
}
}
Theory
Animation is hard, I mean, really good animation is hard. There is a lot of theory which goes into creating good animation, things like easement, anticipation, squish ... I could go on, but I'm boring myself.
The point is, simply incrementing a value (AKA linear progression) is a poor approach to animation. If the system is slow, busy or for some other reason isn't keeping up, the animation will suffer because of it (stuttering, pauses, etc).
A "better" solution is to use a time based progression. That is, you specify the amount of time it will take to move from the current state to it's new state and then continuously loop and update the state until you run out of time.
The "main loop"
If you do any research into game development, they always talk about this thing called the "main loop".
The "main loop" is responsible for updating the game state and scheduling paint passes.
In terms to your question, you need a "main loop" which can update the position of the oval until it reaches it's target position.
Because most GUI frameworks are already running within their own thread context, you need to setup your "main loop" in another thread
AWT
Some theory
AWT is the original GUI framework, so it's "old". While Swing does sit on top of it, you'll find more people have experience with Swing then they do AWT.
One of the important things to keep in mind is, Canvas is not double buffered, so, if you're updating the component fast enough, it will flash.
To overcome this, you need to implement some kind of double buffering workflow.
Runnable example
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.Instant;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
Frame frame = new Frame();
frame.add(new TestCanvas());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Ticker implements Runnable {
public interface Callbck {
public void didTick(Ticker ticker);
}
private boolean isRunning = false;
private Thread thread;
private Callbck callback;
public void setCallback(Callbck tick) {
this.callback = tick;
}
public void start() {
if (isRunning) {
return;
}
isRunning = true;
thread = new Thread(this);
thread.setDaemon(false);
thread.start();
}
public void stop() {
if (!isRunning) {
return;
}
isRunning = false;
thread.interrupt();
thread = null;
}
#Override
public void run() {
while (isRunning) {
try {
Thread.sleep(5);
if (callback != null) {
callback.didTick(this);
}
} catch (InterruptedException ex) {
isRunning = false;
}
}
}
}
public class TestCanvas extends Canvas {
private BufferedImage buffer;
int posX;
private Ticker ticker;
private Instant startedAt;
private Duration duration = Duration.ofSeconds(5);
public TestCanvas() {
ticker = new Ticker();
ticker.setCallback(new Ticker.Callbck() {
#Override
public void didTick(Ticker ticker) {
if (startedAt == null) {
startedAt = Instant.now();
}
Duration runtime = Duration.between(startedAt, Instant.now());
double progress = runtime.toMillis() / (double)duration.toMillis();
if (progress >= 1.0) {
stopAnimation();
}
posX = (int)(getWidth() * progress);
repaint();
}
});
}
protected void startAnimtion() {
ticker.start();
}
protected void stopAnimation() {
ticker.stop();
}
#Override
public void setBounds(int x, int y, int width, int height) {
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
super.setBounds(x, y, width, height);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void addNotify() {
super.addNotify();
startAnimtion();
}
#Override
public void removeNotify() {
super.removeNotify();
buffer = null;
}
#Override
public void paint(Graphics g) {
super.paint(g);
if (buffer == null) {
return;
}
Graphics2D g2d = buffer.createGraphics();
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setColor(Color.RED);
int midY = getHeight() / 2;
g2d.fillOval(posX, midY - 5, 10, 10);
g2d.dispose();
g.drawImage(buffer, 0, 0, this);
}
}
}
What is Canvas good for ...?
In most cases, you should avoid using Canvas, for many of the reasons mentioned above, but one of the reasons you might consider using Canvas is if you want to take full control over the painting process. You might do this if you want to create a complex game which and you want to get the best possible performance out of the rendering pipeline.
See BufferStrategy and BufferCapabilities and the JavaDocs for more detail
A Swing based implementation
Hopefully I've convinced you that a Swing implementation might be a better solution, which in that case you should make use of a Swing Timer instead of Thread, as Swing is not thread safe
See Concurrency in Swing and How to Use Swing Timers
for more details
Runnable example
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.time.Duration;
import java.time.Instant;
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 TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Ticker {
public interface Callbck {
public void didTick(Ticker ticker);
}
private Timer timer;
private Callbck callback;
public void setCallback(Callbck tick) {
this.callback = tick;
}
public void start() {
if (timer != null) {
return;
}
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (callback == null) {
return;
}
callback.didTick(Ticker.this);
}
});
timer.start();
}
public void stop() {
if (timer == null) {
return;
}
timer.stop();
timer = null;
}
}
public class TestPane extends JPanel {
int posX;
private Ticker ticker;
private Instant startedAt;
private Duration duration = Duration.ofSeconds(5);
public TestPane() {
ticker = new Ticker();
ticker.setCallback(new Ticker.Callbck() {
#Override
public void didTick(Ticker ticker) {
if (startedAt == null) {
startedAt = Instant.now();
}
Duration runtime = Duration.between(startedAt, Instant.now());
double progress = runtime.toMillis() / (double) duration.toMillis();
if (progress >= 1.0) {
stopAnimation();
}
posX = (int) (getWidth() * progress);
repaint();
}
});
}
protected void startAnimtion() {
ticker.start();
}
protected void stopAnimation() {
ticker.stop();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void addNotify() {
super.addNotify();
startAnimtion();
}
#Override
public void removeNotify() {
super.removeNotify();
stopAnimation();
}
#Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
int midY = getHeight() / 2;
g2d.fillOval(posX, midY - 5, 10, 10);
g2d.dispose();
}
}
}
The reason this doesn't animate is that nothing triggers the component to update and repaint itself. There are a few things that need to be considered:
Something needs to call the update method. Ordinarily, this is triggered by a call to repaint() on the component, but nothing in this code calls that method.
It's important for an overridden update method to call super.update(g) to ensure the default behavior is invoked (clearing the canvas and painting it again).
Animation has a time component: the oval should move over some period of time. This needs to be incorporated into the logic. AWT has no built-in mechanism for timed behavior.
If you're able to use classes from Swing, the javax.swing.Timer class is very useful for animation. It executes your callback on the AWT thread, and therefore means that you don't have to take special measures to ensure thread safety.
If you can't use Swing, it can use java.util.Timer or a custom thread, but will need to manage thread synchronization directly.
You'll probably also want the animation to stop once the oval reaches the edge of the canvas.
Here's an example using javax.swing.Timer (assuming Java 8 or later). Note that all of the animation logic is in the ActionListener attached to the Timer, so the overridden update method has been removed:
import javax.swing.*;
import java.awt.*;
public class MainClass {
public static final int CANVAS_SIZE = 400;
public MainClass() {
Frame f = new Frame("Canvas Example");
f.add(new MyCanvas(CANVAS_SIZE));
f.setLayout(null);
f.setSize(CANVAS_SIZE, CANVAS_SIZE);
f.setVisible(true);
}
public static void main(String[] args) {
new MainClass();
}
}
class MyCanvas extends Canvas {
public static final int INITIAL_POSITION = 75;
public static final int HEIGHT = 75;
public static final int WIDTH = 150;
private static final int TIMER_DELAY_MILLIS = 1000 / 30; // 30 FPS
private int x = INITIAL_POSITION;
private final Timer timer;
public MyCanvas(int canvasSize) {
setBackground(Color.BLACK);
setSize(canvasSize, canvasSize);
timer = new Timer(TIMER_DELAY_MILLIS, (event) -> {
// ensure the oval stays on the canvas
if (x + WIDTH < getWidth()) {
x++;
repaint();
} else {
stopAnimation();
}
});
timer.start();
}
public void paint(Graphics g) {
g.setColor(Color.red);
g.fillOval(x, INITIAL_POSITION, WIDTH, HEIGHT);
}
private void stopAnimation() {
timer.stop();
}
}
This code has a few additional incidental changes.
Updated the name of mainClass to MainClass (leading capital "M") to comply with standard Java naming conventions.
Changed String args[] to String[] args for the same reason.
Extracted numeric constants to named static final fields.
Made the canvas size a constructor parameter, controlled by the caller.
Made x private.
Minor formatting changes to ensure a consistent style.
One option that doesn't use javax.swing.Timer (with unchanged code omitted):
private final AtomicInteger x = new AtomicInteger(INITIAL_POSITION);
public MyCanvas(int canvasSize) {
setBackground(Color.BLACK);
setSize(canvasSize, canvasSize);
new Thread(() -> {
try {
// ensure the oval stays on the canvas
while (x.incrementAndGet() + WIDTH < getWidth()) {
Thread.sleep(TIMER_DELAY_MILLIS);
repaint();
}
} catch (InterruptedException e) {
// Just let the thread exit
Thread.currentThread().interrupt();
}
}).start();
}

Ball doesn't move; Thread?

This is an UI that makes a ball go down in a diagonal way, but the ball stays static; it seems something is not working adecuatedly with the threads. Could you please, tell me how to make the ball move?
Please download a ball and change the directory so the program can find where your ball is allocated. It's not necessary to download the soccer pitch but if you want, it's OK. Finally, I have to thank you for spending time in search of this malfunctioning.
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.event.ActionListener;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import java.io.File;
class Animation extends JFrame implements ActionListener { //Frame and listener
Rectangle2D dimensions = new Rectangle2D.Double(0,0,850,595); //Not implemented limits
JButton animate, stop;
Runnable runnable;
Thread move;
public Animation() {
setLayout(new BorderLayout()); //BorderLayout disposition
setTitle("Pelota en acción");
animate = new JButton("Animate it!"); //Button to create balls
animate.setBounds(0,0,120,30);
animate.addActionListener(new ActionListener(){
#Override
public void actionPerformed(ActionEvent e) {
Image ball = null;
new Layout().createEllipse(ball);
runnable = new Layout();
move = new Thread(runnable);
move.start();
}
});
stop = new JButton("Freeze"); //Button to interrupt thread (not implemented)
stop.setBounds(0,0,120,30);
stop.addActionListener(new ActionListener(){
#Override
public void actionPerformed(ActionEvent e) {
move.interrupt();
Layout.running = false;
}
});
JPanel subPanel = new JPanel(); //Layout with its buttons situated to the south
subPanel.add(animate);
subPanel.add(stop);
add(subPanel,BorderLayout.SOUTH);
add(new Layout());
}
public static void main(String[] args) {
Animation ventana = new Animation();
ventana.setSize(850,625);
ventana.setLocationRelativeTo(null);
ventana.setVisible(true);
ventana.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
#Override
public void actionPerformed(ActionEvent e) {} //Tag
} //Class close
class Layout extends JPanel implements Runnable { //Layout and thread
int X,Y; //Coordenadas
static boolean running = true; //"To interrupt the thread" momentaneously.
static ArrayList<Image> balls = new ArrayList<>(); //Balls collection
#Override
public void run () { //Just moves ball towards Narnia xd
while(running) {
X++; Y++;
System.out.println(X+" "+Y);
repaint();
updateUI();
try {
Thread.sleep(4);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
repaint();
updateUI();
try {
URL url = new URL("https://www.freejpg.com.ar/image-900/9c/9ca2/F100004898-textura_pasto_verde_linea_de_cal.jpg");
Image picture = ImageIO.read(url);
g.drawImage(picture,0,0,null);
} catch(IOException e){
System.out.println("URL image was not found");
}
finally {
try {
//----------------------------------------------------------------------------
Image picture = ImageIO.read(new File("C:\\Users\\Home\\Desktop\\Cancha.jpg")); //Pitch
//----------------------------------------------------------------------------
g.drawImage(picture, 0, 0, null);
} catch (IOException ex) {
System.out.println("Pitch image was not found");
}
}
for (Image ball : balls) { //I add balls to the Layout
g2.drawImage(ball,X,Y,100,100,null);
}
}
public void createEllipse (Image ball) { //Method that adds balls to the collection
try {
//-------------------------------------------------------------------- Ball
ball = ImageIO.read(new File("C:\\Users\\Home\\Desktop\\Pelota.png")); //Change this
//-------------------------------------------------------------------- Ball
} catch(IOException ex) {
System.out.println("Any balls were found");
}
balls.add(ball);
}
}
So to break your code down:
When the button is pressed, you execute the following code:
Image ball = null;
new Layout().createEllipse(ball);
runnable = new Layout();
move = new Thread(runnable);
move.start();
This will create a new layout. The run() method of this will increase the X and Y variables. They are declared here:
int X,Y; //Coordenadas
Those are instance variables, this means they belong to your newly created Layout.
Then you call repaint() on the new Layout, which will do nothing, because this new Layout has not been added to some window.
So, how do you fix this?
First, you have to keep the original Layout around:
class Animation extends JFrame { // no need to implement ActionListener
Rectangle2D dimensions = new Rectangle2D.Double(0,0,850,595); //Not implemented limits
JButton animate, stop;
Thread move;
Layout layout;
Then remember the Layout when you create it:
// before: add(new Layout());
layout = new Layout();
add(layout);
Then use the layout in your ActionListener:
layout.createEllipse(ball);
move = new Thread(layout);
move.start();
This might have some problems with concurrency (Swing is not thread-safe), so for good measure, you should call repaint() in the AWTEventThread:
// in run(), was repaint():
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
repaint();
}
});
Now, there are some cleanup tasks left:
Delete this code:
#Override
public void actionPerformed(ActionEvent e) {} //Tag
It's no longer needed, because you don't implement ActionListener.
Drop the static modifiers from some fields, and add volatile:
volatile int X,Y; //Coordenadas
volatile boolean running = true; //"To interrupt the thread" momentaneously.
ArrayList<Image> balls = new ArrayList<>(); //Balls collection
volatile is needed for variables that are accessed from more than one thread.
Also remove repaint() and resetUI() from the paint method. You don't need them.
For the pictures in paint: you should cache them. Store them in a field, so you don't have to load the picture every time.
When all this is done, your code is much cleaner, but there are still some warts that should be addressed. But at least you have something working.
Johannes has already spoken about many of the things which are wrong with your original example, so I won't go over many of them again.
This example makes use of a Swing Timer instead of a Thread as the main "animation" loop. It also focuses on demonstrating encapsulation and responsibility.
For example, the AnimtionPane is responsible for managing the balls, managing the animation loop and paint. It isn't, however, responsible for determining "how" the balls are updated or paint, it only provides the timing and functionality to make those things happen.
A couple of the glaring issues I can see are:
Trying to load resources from within the paintComponent method. This is a bad ideas, as it could slow you paint pass down, causing your UI to lag
Calling repaint and updateUI from within the paintComponent method. You should avoid causing any new updates to the UI from occurring during a paint process. This could cause your program to run wide and consume all the CPU cycles, not only making your app non-responsive, but also the whole system.
Some very quick points
Swing is not thread safe. You should never update the UI (or anything the UI relies on) from outside the context of the Event Dispatching Thread. This example uses a Swing Timer as it allows the delay to occur of the EDT (and not block the UI), but it's updates are triggered within the EDT, allowing us to safely update the UI from within
You create multiple instances of Layout, meaning that the one on the screen isn't the one which is been updated
Your "freeze" logic is broken. It will never "freeze" anything
Runnable example
import java.awt.BorderLayout;
import java.awt.Color;
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.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JButton;
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 TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private AnimationPane animationPane;
public TestPane() {
setLayout(new BorderLayout());
animationPane = new AnimationPane();
JButton actionButton = new JButton("Start");
actionButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
if (animationPane.isAnimating()) {
animationPane.stop();
actionButton.setText("Start");
} else {
animationPane.start();
actionButton.setText("Stop");
}
}
});
add(animationPane);
add(actionButton, BorderLayout.SOUTH);
}
}
// This is just makes it seem more random ;)
private static Random RANDOM = new Random();
public class Ball {
private int x;
private int y;
private int xDelta;
private int yDelta;
private Color color;
private Shape shape;
public Ball(Color color) {
shape = new Ellipse2D.Double(0, 0, 10, 10);
this.color = color;
// Get some random motion
do {
xDelta = RANDOM.nextInt(6) + 2;
yDelta = RANDOM.nextInt(6) + 2;
} while (xDelta == yDelta);
}
public void update(Rectangle bounds) {
x += xDelta;
y += yDelta;
if (x + 10 > bounds.x + bounds.width) {
x = bounds.x + bounds.width - 10;
xDelta *= -1;
} else if (x < bounds.x) {
x = bounds.x;
xDelta *= -1;
}
if (y + 10 > bounds.y + bounds.height) {
y = bounds.y + bounds.height - 10;
yDelta *= -1;
} else if (y < bounds.y) {
y = bounds.y;
yDelta *= -1;
}
}
public void paint(Graphics2D g2d) {
// This makes it easier to restore the graphics context
// back to it's original state
Graphics2D copy = (Graphics2D) g2d.create();
copy.setColor(color);
copy.translate(x, y);
copy.fill(shape);
// Don't need the copy any more, get rid of it
copy.dispose();
}
}
public class AnimationPane extends JPanel {
// This does not need to be static
private List<Ball> balls = new ArrayList<>(); //Balls collection
private Timer timer;
private List<Color> colors;
public AnimationPane() {
colors = new ArrayList<>(8);
colors.add(Color.RED);
colors.add(Color.GREEN);
colors.add(Color.BLUE);
colors.add(Color.CYAN);
colors.add(Color.MAGENTA);
colors.add(Color.ORANGE);
colors.add(Color.PINK);
colors.add(Color.YELLOW);
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
if (RANDOM.nextBoolean()) {
makeBall();
}
Rectangle bounds = new Rectangle(new Point(0, 0), getSize());
for (Ball ball : balls) {
ball.update(bounds);
}
repaint();
}
});
makeBall();
}
protected void makeBall() {
Collections.shuffle(colors);
balls.add(new Ball(colors.get(0)));
}
public boolean isAnimating() {
return timer.isRunning();
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
// Bad ideas. Repaint will cause a new paint event to be posted, causing your
// UI to run away - consuming all your CPU cycles in a singulator forms
// and destorys the known universe
//repaint();
// This doesn't do what you think it does and there shouldn't be
// reason for you to call it
//updateUI();
// This is a bad idea as it could cause the paint cycles to slow down
// destorying the responsiveness of your app
// Besids, you should be passing this as the ImageObserver
// try {
// URL url = new URL("https://www.freejpg.com.ar/image-900/9c/9ca2/F100004898-textura_pasto_verde_linea_de_cal.jpg");
// Image picture = ImageIO.read(url);
// g.drawImage(picture, 0, 0, null);
// } catch (IOException e) {
// System.out.println("URL image was not found");
// } finally {
// try {
// //----------------------------------------------------------------------------
// Image picture = ImageIO.read(new File("C:\\Users\\Home\\Desktop\\Cancha.jpg")); //Pitch
// //----------------------------------------------------------------------------
// g.drawImage(picture, 0, 0, null);
// } catch (IOException ex) {
// System.out.println("Pitch image was not found");
// }
// }
// This is "bad" per say, but each ball should have it's own
// concept of location
// for (Image ball : balls) { //I add balls to the Layout
// g2.drawImage(ball, X, Y, 100, 100, null);
// }
for (Ball ball : balls) {
ball.paint(g2);
}
// I made a copy of the graphics context, as this is shared
// with all the other components been painted, changing the
// render hints could cause issues
g2.dispose();
}
}
}

How can I make a polygon object in Java pulsate (like the chalice in the Atari game "Adventure")

This is what I have in my paintComponent (most other things omitted, just the stuff that pertains to an Item object called chalice with a polygon field, the explicit parameters of the if statement are not important for this question)
Currently, it shows up as solid white because I set the color to all 255, but I want to make it gradually transition to different colors smoothly, not strobing, more like pulsating but I don't really know what that is called. I was thinking about replacing the explicit parameters of the Color with arrays that cycle through numbers in that array and somehow link that to a TimerListener, but I am new to graphics so I am not sure if that is the best way to go about this.
public void paintComponent(Graphics g) {
Graphics2D sprite = (Graphics2D) g;
if (chalice.getHolding() == true || roomID == chalice.getRoomDroppedIn()) {
sprite.setColor(new Color(255, 255, 255));
sprite.fill(chalice.getPoly());
}
}
Some basic concepts...
A pulsating effect needs to move in two direction, it needs to fade in AND out
In order to know how much "effect" should be applied, two things need to be known. First, how long the overall effect takes to cycle (from fully opaque to fully transparent and back again) and how far through the cycle the animation is.
This is not a simple thing to manage, there's a lot of "stateful" information which needs to be managed and maintained, and normally, done so separately from other effects or entities.
To my mind, the simplest solution is to devise some kind of "time line" which manages key points (key frames) along the time line, calculates the distance between each point and the value that it represents.
Take a step back for a second. We know that at:
0% we want to be fully opaque
50% we want to be fully transparent
100% we want to be fully opaque
The above takes into consideration that we want to "auto reverse" the animation.
The reason for working with percentages, is that it allows us to define a timeline of any given duration and the timeline will take care of the rest. Where ever possible, always work with normalised values like this, it makes the whole thing a lot simpler.
TimeLine
The following is a pretty simple concept of a "timeline". It has a Duration, the time over which the timeline is played, key frames, which provide key values over the duration of the timeline and the means to calculate a specific value at a specific point over the life of the timeline.
This implementation also provides "auto" replay-ability. That is, if the timeline is played "over" it's specified Duration, rather then stopping, it will automatically reset and take into consideration the amount of time "over" as part of it's next cycle (neat)
public class TimeLine {
private Map<Float, KeyFrame> mapEvents;
private Duration duration;
private LocalDateTime startedAt;
public TimeLine(Duration duration) {
mapEvents = new TreeMap<>();
this.duration = duration;
}
public void start() {
startedAt = LocalDateTime.now();
}
public boolean isRunning() {
return startedAt != null;
}
public float getValue() {
if (startedAt == null) {
return getValueAt(0.0f);
}
Duration runningTime = Duration.between(startedAt, LocalDateTime.now());
if (runningTime.compareTo(duration) > 0) {
runningTime = runningTime.minus(duration);
startedAt = LocalDateTime.now().minus(runningTime);
}
long total = duration.toMillis();
long remaining = duration.minus(runningTime).toMillis();
float progress = remaining / (float) total;
return getValueAt(progress);
}
public void add(float progress, float value) {
mapEvents.put(progress, new KeyFrame(progress, value));
}
public float getValueAt(float progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
float max = keyFrames[1].progress - keyFrames[0].progress;
float value = progress - keyFrames[0].progress;
float weight = value / max;
float blend = blend(keyFrames[0].getValue(), keyFrames[1].getValue(), 1f - weight);
return blend;
}
public KeyFrame[] getKeyFramesBetween(float progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
if (startAt >= keyFrames.length) {
startAt = keyFrames.length - 1;
}
frames[0] = mapEvents.get(keyFrames[startAt - 1]);
frames[1] = mapEvents.get(keyFrames[startAt]);
return frames;
}
protected float blend(float start, float end, float ratio) {
float ir = (float) 1.0 - ratio;
return (float) (start * ratio + end * ir);
}
public class KeyFrame {
private float progress;
private float value;
public KeyFrame(float progress, float value) {
this.progress = progress;
this.value = value;
}
public float getProgress() {
return progress;
}
public float getValue() {
return value;
}
#Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
Setting up the timeline is pretty simple...
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
We give a specified Duration and setup the key frame values. After that we just need to "start" it and get the current value from the TimeLine based on how long it's been playing.
This might seem like a lot of work for what seems like a simple problem, but remember, this is both dynamic and re-usable.
It's dynamic in that you can provide any Duration you want, thus changing the speed, at it will "just work", and re-usable, as you can generate multiple instances for multiple entities and it will be managed independently.
Example...
The following example simply makes use of Swing Timer to act as the "main-loop" for the animation. On each cycle, it asks the TimeLine for the "current" value, which simply acts as the alpha value for the "pulsating" effect.
The TimeLine class itself is decoupled enough that it won't matter "how" you've establish your "main-loop", you simply start it running and pull the "current" value from it when ever you can...
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.TreeMap;
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 TimeLine timeLine;
private float alpha = 0;
public TestPane() {
timeLine = new TimeLine(Duration.ofSeconds(5));
timeLine.add(0.0f, 1.0f);
timeLine.add(0.5f, 0.0f);
timeLine.add(1.0f, 1.0f);
Timer timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (!timeLine.isRunning()) {
timeLine.start();
}
alpha = timeLine.getValue();
repaint();
}
});
timer.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setComposite(AlphaComposite.SrcOver.derive(alpha));
g2d.setColor(Color.RED);
g2d.fill(new Rectangle(45, 45, 110, 110));
g2d.dispose();
g2d = (Graphics2D) g.create();
g2d.setColor(getBackground());
g2d.fill(new Rectangle(50, 50, 100, 100));
g2d.setColor(Color.BLACK);
g2d.draw(new Rectangle(50, 50, 100, 100));
g2d.dispose();
}
}
}
I would tie up the TimeLine as part of the effect for the specified entity. This would tie the TimeLine to a specific entity, meaning that many entities could all have their own TimeLines calculating a verify of different values and effects
"Is there a simpler solution?"
This is a subjective question. There "might" be a "simpler" approach which will do the same job, but which won't be as scalable or re-usable as this kind of approach.
Animation is a complex subject, trying to make it work in a complex solution, running multiple different effects and entities just compounds the problem
I've toyed with the idea of making the TimeLine generic so it could be used to generate a verity of different values based on the desired result, making it a much more flexible and re-usable solution.
Color blending ....
I don't know if this is a requirement, but if you have a series of colors you want to blend between, a TimeLine would also help you here (you don't need the duration so much). You could set up a series of colors (acting as key frames) and calculate which color to use based on the progression of the animation.
Blending colors is somewhat troublesome, I've spent a lot of time trying to find a decent algorithm which works for me, which is demonstrated at Color fading algorithm? and Java: Smooth Color Transition

What is the correct way to use createBufferStrategy()?

Even after using Java Swing for over a year, it still seems like magic to me. How do I correctly use a BufferStrategy, in particular, the method createBufferSrategy()?
I would like to have a JFrame and a Canvas that gets added to it and then painted. I would also like to be able to resize (setSize()) the Canvas. Every time I resize the Canvas it seems my BufferStrategy gets trashed or rather, turns useless, since using show() on the BufferStrategy does not actually do anything. Also, createBufferStrategy() has a weird non-deterministic behaviour and I don't know how to synchronize it correctly.
Here's what I mean:
public class MyFrame extends JFrame {
MyCanvas canvas;
int i = 0;
public MyFrame() {
setUndecorated(false);
setVisible(true);
setSize(1100, 800);
setLocation(100, 100);
setDefaultCloseOperation(EXIT_ON_CLOSE);
canvas = new MyCanvas();
add(canvas);
canvas.makeBufferStrat();
}
#Override
public void repaint() {
super.repaint();
canvas.repaint();
//the bigger threshold's value, the more likely it is that the BufferStrategy works correctly
int threshold = 2;
if (i < threshold) {
i++;
canvas.makeBufferStrat();
}
}
}
MyCanvas has a method makeBufferStrat() and repaint():
public class MyCanvas extends Canvas {
BufferStrategy bufferStrat;
Graphics2D g;
public MyCanvas() {
setSize(800, 600);
setVisible(true);
}
public void makeBufferStrat() {
createBufferStrategy(2);
//I'm not even sure whether I need to dispose() those two.
if (g != null) {
g.dispose();
}
if (bufferStrat != null) {
bufferStrat.dispose();
}
bufferStrat = getBufferStrategy();
g = (Graphics2D) (bufferStrat.getDrawGraphics());
g.setColor(Color.BLUE);
}
#Override
public void repaint() {
g.fillRect(0, 0, 100, 100);
bufferStrat.show();
}
}
I simply call MyFrame's repaint() method from a while(true) loop in the main method.
When threshold is small (i.e. 2), bufferStrat.show() in about 70% of all cases doesn't do anything - the JFrame just remains gray upon starting the program. The remaining 30% it paints the rectangle how it's supposed to. If I do threshold = 200;, the painting succeeds close to 100% of the time I execute the program. Javadoc says that createBufferStrategy() may take a while, so I assume that's the issue here. However, how do I synchronize and use it properly? Clearly, I'm doing something wrong here. I can't imagine that's how it's supposed to be used.
Does anyone have a minimal working example?
The way you create the BufferStrategy is "okay", you could have a look at the JavaDocs for BufferStrategy which has a neat little example.
The way you're using it, is questionable. The main reason for using a BufferStrategy is because you want to take control of the painting process (active painting) away from Swing's painting algorithm (which is passive)
BUT, you seem to trying to do both, which is why it's causing your issues. Instead, you should have a "main" loop which is responsible for deciding what and when the buffer should paint, for example...
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferStrategy;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
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();
}
TestPane testPane = new TestPane();
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(testPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
// The component needs to be attached to displayed window before
// the buffer can be created
testPane.startPainting();
}
});
}
public class TestPane extends Canvas {
private AtomicBoolean painting = new AtomicBoolean(true);
private PaintCycle paintCycle;
private Rectangle clickBounds;
public TestPane() {
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (clickBounds != null && clickBounds.contains(e.getPoint())) {
painting.set(false);
}
}
});
}
public void startPainting() {
if (paintCycle == null) {
createBufferStrategy(2);
painting.set(true);
paintCycle = new PaintCycle();
Thread t = new Thread(paintCycle);
t.setDaemon(true);
t.start();
}
}
public void stopPainting() {
if (paintCycle != null) {
painting.set(false);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
public class PaintCycle implements Runnable {
private BufferStrategy strategy;
private int xDelta = 2;
private int yDelta = 2;
#Override
public void run() {
System.out.println("Painting has started");
int x = (int) (Math.random() * (getWidth() - 40));
int y = (int) (Math.random() * (getHeight() - 40));
do {
xDelta = (int) (Math.random() * 8) - 4;
} while (xDelta == 0);
do {
yDelta = (int) (Math.random() * 8) - 4;
} while (yDelta == 0);
clickBounds = new Rectangle(x, y, 40, 40);
strategy = getBufferStrategy();
while (painting.get()) {
// Update the state of the model...
update();
// Paint the state of the model...
paint();
try {
// What ever calculations you want to use to maintain the framerate...
Thread.sleep(40);
} catch (InterruptedException ex) {
}
}
System.out.println("Painting has stopped");
}
protected void update() {
int x = clickBounds.x + xDelta;
int y = clickBounds.y + yDelta;
if (x + 40 > getWidth()) {
x = getWidth() - 40;
xDelta *= -1;
} else if (x < 0) {
x = 0;
xDelta *= -1;
}
if (y + 40 > getHeight()) {
y = getHeight() - 40;
yDelta *= -1;
} else if (y < 0) {
y = 0;
yDelta *= -1;
}
clickBounds.setLocation(x, y);
}
protected void paint() {
// Render single frame
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics2D graphics = (Graphics2D) strategy.getDrawGraphics();
// Render to graphics
// ...
graphics.setColor(Color.BLUE);
graphics.fillRect(0, 0, getWidth(), getHeight());
graphics.setColor(Color.RED);
graphics.fill(clickBounds);
// Dispose the graphics
graphics.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (strategy.contentsRestored());
// Display the buffer
strategy.show();
// Repeat the rendering if the drawing buffer was lost
} while (strategy.contentsLost());
}
}
}
}
You should also remember, Swing's been using either DirectX or OpenGL pipelines since about 1.4 (or maybe 1.5). The main reasons for using BufferStrategy are more direct access to the hardware (which Swing is pretty close to anyway) AND direct control over the painting process (which is now really the only reason to use it)

Categories

Resources