I'm trying to make my Pedestrian object move, and it moves but at a certain point it flies away from the screen. The Pedestrian moves by a List of points. First the Pedestrian is added to toDraw to paint it and in startAndCreateTimer I loop through the same list to move the Vehicles Maybe it's because of this line i = (double) diff / (double) playTime; I actually don't want to set a playtime how not to do that, could this be the problem or is it something else? Here a link with the point where the Pedestrian flies away (starts north of left roundabout) http://gyazo.com/23171a6106c88f1ba8ca438598ff4153.
class Surface extends JPanel{
Track track=new Track();
public List<Vehicle> toDraw = new ArrayList<>();
private Long startTime;
private long playTime = 4000;
private double i;
public Surface(){
startAndCreateTimer();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//Make sure the track is painted first
track.paint(g);
for (Vehicle v : toDraw) {
v.paint(g);
}
}
public void repaintPanel(){
this.repaint();
}
private void startAndCreateTimer(){
Timer timer = new Timer(100, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (startTime == null) {
startTime = System.currentTimeMillis();
}
long now = System.currentTimeMillis();
long diff = now - startTime;
i = (double) diff / (double) playTime;
for (Vehicle v : toDraw){
v.update(i);
}
repaintPanel();
}
});
timer.start();
}
}
Pedestrian java
public class Pedestrian extends Vehicle {
BufferedImage pedestrian;
Point pedestrianPosition;
double pedestrianRotation = 0;
int pedestrianW, pedestrianH;
int counter=0;
List<LanePoint>pedestrianPath;
boolean lockCounter=false;
public Pedestrian(int x, int y){
try {
pedestrian = ImageIO.read(Car.class.getResource("images/human.png"));
} catch (IOException e) {
System.out.println("Problem loading pedestrian images: " + e);
}
pedestrianPosition = new Point(x,y);
pedestrianW = pedestrian.getWidth();
pedestrianH = pedestrian.getHeight();
}
#Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.rotate(Math.toRadians(pedestrianRotation), pedestrianPosition.x, pedestrianPosition.y);
g2d.drawImage(pedestrian, pedestrianPosition.x, pedestrianPosition.y, null);
}
#Override
public void setPath(List<LanePoint> path) {
pedestrianPath=path;
}
/*Update*/
#Override
public void update(double i){
if (counter < pedestrianPath.size()) {
Point startPoint = new Point(pedestrianPosition.x, pedestrianPosition.y);
LanePoint endPoint = new LanePoint(pedestrianPath.get(counter).x, pedestrianPath.get(counter).y,pedestrianPath.get(counter).lanePointType,pedestrianPath.get(counter).lanePointToTrafficLight,pedestrianPath.get(counter).laneTrafficLightId,pedestrianPath.get(counter).degreesRotation);
pedestrianPosition.x=(int)Maths.lerp(startPoint.x,endPoint.x,i);
pedestrianPosition.y=(int)Maths.lerp(startPoint.y,endPoint.y,i);
pedestrianRotation=endPoint.degreesRotation;
if(pedestrianPosition.equals(new Point(endPoint.x,endPoint.y))){
/*PEDESTRIAN SIGN UP*/
if (endPoint.lanePointType.equals(LanePoint.PointType.TRAFFICLIGHT) && endPoint.lanePointToTrafficLight.equals(LanePoint.PointToTrafficLight.INFRONTOF)){
try {
Roundabout.client.sendBytes(new byte []{0x03,endPoint.laneTrafficLightId.byteValue(),0x01,0x00});
} catch (IOException ex) {
ex.printStackTrace();
}
}
/*PEDESTRIAN SIGN OFF*/
else if (endPoint.lanePointType.equals(LanePoint.PointType.TRAFFICLIGHT) && endPoint.lanePointToTrafficLight.equals(LanePoint.PointToTrafficLight.UNDERNEATH)) {
if (Surface.trafficLights.get(endPoint.laneTrafficLightId).red) {
lockCounter = true;
} else {
try {
Roundabout.client.sendBytes(new byte[]{0x03, endPoint.laneTrafficLightId.byteValue(), 0x00, 0x00});
lockCounter=false;
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
if (!lockCounter) {
counter++; //Increment counter > sets next point
}
}
}
}
}
Maths.java
public class Maths {
//Lineat interpolation
public static double lerp(double a, double b, double t) {
return a + (b - a) * t;
}
}
So, basically you are calculating the position of the object between to points based on the amount of time that has passed. This is good.
So at t = 0, the object will be at the start point, at t = 0.5, it will be halfway between the start and end point, at t = 1.0 it will be at the end point.
What happens when t > 1.0? Where should the object be? - hint, it should be nowhere as it should have been removed or reset...
This and this are basic examples of "time line" based animation, meaning that, over a period of time, the position of the object is determined by using different points (along a time line)
So, in order to calculate the position along a line, you need three things, the point you started at, the point you want to end at and the duration (between 0-1)
Using these, you can calculate the point along the line between these two points based on the amount of time.
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
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 static class TestPane extends JPanel {
protected static final double PLAY_TIME = 4000.0;
private Point2D startAt = new Point(0, 0);
private Point2D endAt = new Point(200, 200);
private Point2D current = startAt;
private Long startTime;
public TestPane() {
Timer timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (startTime == null) {
startTime = System.currentTimeMillis();
}
long time = System.currentTimeMillis() - startTime;
double percent = (double) time / PLAY_TIME;
if (percent > 1.0) {
percent = 1.0;
((Timer) e.getSource()).stop();
}
current = calculateProgress(startAt, endAt, percent);
repaint();
}
});
timer.start();
}
protected Point2D calculateProgress(Point2D startPoint, Point2D targetPoint, double progress) {
Point2D point = new Point2D.Double();
if (startPoint != null && targetPoint != null) {
point.setLocation(
calculateProgress(startPoint.getX(), targetPoint.getY(), progress),
calculateProgress(startPoint.getX(), targetPoint.getY(), progress));
}
return point;
}
protected double calculateProgress(double startValue, double endValue, double fraction) {
return startValue + ((endValue - startValue) * fraction);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.GREEN);
g2d.draw(new Line2D.Double(startAt, endAt));
g2d.setColor(Color.RED);
g2d.fill(new Ellipse2D.Double(current.getX() - 5, current.getY() - 5, 10, 10));
g2d.dispose();
}
}
}
So, using current = calculateProgress(startAt, endAt, percent);,
you can see that the dot moves evenly between the start and end points.
If we change it to something more like what you seem to be doing, current = calculateProgress(current, endAt, percent);,
you can see that it speeds down the line and finally eases out, which isn't what you really want...
Updated with time line theory
Let's imagine you have a time line, which has a length of t and along this time line, you have 5 events (or key frames) (e1 - e5), each occurring after each other.
e1 starts at 0 and e5 ends at 1
As you can see, the events occur at irregular intervals and run for different lengths of time.
t1 runs for 25% of the time line
t2 runs for 25% of the time line
t3 runs for 12.5% of the time line
t3 runs for 37.5% of the time line
So, based on t, you need to determine which events are been executed. So when t is 0.12, we are running about half way through t1 (between e1 & e2).
You then need to calculate local time/difference between the key frames (0-0.25 along the timeline)
localTime = 1.0 - ((t - e1) / (e2 - e1))
= 1.0 - ((0.12 - 0) / (0.25 - 0))
= 1.0 - (0.12 / 0.25)
= 1.0 - 0.48
= 0.52
Where t is the time along the time line, e1 is the time of the first event (0) and e2 is the time of the second event (0.25), which gives us the duration along the t1 (in this example)
This is then the value of your linear interpolation for the given time slice.
Runnable example...
I took a look at your code, but there's a lot of work that needs to be done to get this to work.
Basically, you need to know how long the path is and the amount that each segment is of that path (as a percentage). With this, we can create a "time line" of "key frames" which determines how far along the "path" your object is based on the amount of time that has passed and the amount of time it "should" take to travel.
So, the first thing I did was create a Path class (kind of mimics your Lists, but has some additional methods)
public class Path implements Iterable<Point> {
private List<Point> points;
private double totalLength = 0;
public Path(Point... points) {
this.points = new ArrayList<>(Arrays.asList(points));
for (int index = 0; index < size() - 1; index++) {
Point a = get(index);
Point b = get(index + 1);
double length = lengthBetween(a, b);
totalLength += length;
}
}
public double getTotalLength() {
return totalLength;
}
public int size() {
return points.size();
}
public Point get(int index) {
return points.get(index);
}
public double lengthBetween(Point a, Point b) {
return Math.sqrt(
(a.getX() - b.getX()) * (a.getX() - b.getX())
+ (a.getY() - b.getY()) * (a.getY() - b.getY()));
}
#Override
public Iterator<Point> iterator() {
return points.iterator();
}
}
Mostly, this provides the totalLength of the path. We use this to calculate how much each segment takes up later
I then borrowed the TimeLine class from this previous answer
public class Timeline {
private Map<Double, KeyFrame> mapEvents;
public Timeline() {
mapEvents = new TreeMap<>();
}
public void add(double progress, Point p) {
mapEvents.put(progress, new KeyFrame(progress, p));
}
public Point getPointAt(double progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
double max = keyFrames[1].progress - keyFrames[0].progress;
double value = progress - keyFrames[0].progress;
double weight = value / max;
return blend(keyFrames[0].getPoint(), keyFrames[1].getPoint(), 1f - weight);
}
public KeyFrame[] getKeyFramesBetween(double progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Double[] keyFrames = mapEvents.keySet().toArray(new Double[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 Point blend(Point start, Point end, double ratio) {
Point blend = new Point();
double ir = (float) 1.0 - ratio;
blend.x = (int) (start.x * ratio + end.x * ir);
blend.y = (int) (start.y * ratio + end.y * ir);
return blend;
}
public class KeyFrame {
private double progress;
private Point point;
public KeyFrame(double progress, Point point) {
this.progress = progress;
this.point = point;
}
public double getProgress() {
return progress;
}
public Point getPoint() {
return point;
}
}
}
Now, as they stand, they are not compatible, we need to take each segment and calculate the length of the segment as a percentage of the total length of the path and create a key frame for the specified point along the time line...
double totalLength = path.getTotalLength();
timeLine = new Timeline();
timeLine.add(0, path.get(0));
// Point on time line...
double potl = 0;
for (int index = 1; index < path.size(); index++) {
Point a = path.get(index - 1);
Point b = path.get(index);
double length = path.lengthBetween(a, b);
double normalised = length / totalLength;
// Normalised gives as the percentage of this segment, we need to
// translate that to a point on the time line, so we just add
// it to the "point on time line" value to move to the next point :)
potl += normalised;
timeLine.add(potl, b);
}
I did this deliberately, to show the work you are going to need to do.
Need, I create a Ticker, which just runs a Swing Timer and reports ticks to Animations
public enum Ticker {
INSTANCE;
private Timer timer;
private List<Animation> animations;
private Ticker() {
animations = new ArrayList<>(25);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Prevent possible mutatation issues...
Animation[] anims = animations.toArray(new Animation[animations.size()]);
for (Animation animation : anims) {
animation.tick();
}
}
});
}
public void add(Animation animation) {
animations.add(animation);
}
public void remove(Animation animation) {
animations.remove(animation);
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
public interface Animation {
public void tick();
}
This centralises the "clock", be allows Animations to determine what they would like to do on each tick. This should be more scalable then creating dozens of Timers
Okay, that's all fun and games, but how does it work together? Well, here's a complete runnable example.
It takes one of your own paths and creates a TimeLine out of it and animates a object moving along it.
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
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();
}
Path path = new Path(
new Point(440, 40),
new Point(440, 120),
new Point(465, 90),
new Point(600, 180),
new Point(940, 165),
new Point(940, 145),
new Point(1045, 105),
new Point(1080, 120),
new Point(1170, 120),
new Point(1200, 120),
new Point(1360, 123),
new Point(1365, 135),
new Point(1450, 170),
new Point(1457, 160),
new Point(1557, 160));
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane(path));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Ticker.INSTANCE.start();
}
});
}
public enum Ticker {
INSTANCE;
private Timer timer;
private List<Animation> animations;
private Ticker() {
animations = new ArrayList<>(25);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
// Prevent possible mutatation issues...
Animation[] anims = animations.toArray(new Animation[animations.size()]);
for (Animation animation : anims) {
animation.tick();
}
}
});
}
public void add(Animation animation) {
animations.add(animation);
}
public void remove(Animation animation) {
animations.remove(animation);
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
}
public interface Animation {
public void tick();
}
public static final double PLAY_TIME = 4000d;
public class TestPane extends JPanel implements Animation {
private Path path;
private Path2D pathShape;
private Timeline timeLine;
private Long startTime;
private Point currentPoint;
public TestPane(Path path) {
this.path = path;
// Build the "path" shape, we can render this, but more importantally
// it allows use to determine the preferred size of the panel :P
pathShape = new Path2D.Double();
pathShape.moveTo(path.get(0).x, path.get(0).y);
for (int index = 1; index < path.size(); index++) {
Point p = path.get(index);
pathShape.lineTo(p.x, p.y);
}
// Build the time line. Each segemnt (the line between any two points)
// makes up a percentage of the time travelled, we need to calculate
// the amount of time that it would take to travel that segement as
// a percentage of the overall length of the path...this
// allows us to even out the time...
double totalLength = path.getTotalLength();
timeLine = new Timeline();
timeLine.add(0, path.get(0));
// Point on time line...
double potl = 0;
for (int index = 1; index < path.size(); index++) {
Point a = path.get(index - 1);
Point b = path.get(index);
double length = path.lengthBetween(a, b);
double normalised = length / totalLength;
// Normalised gives as the percentage of this segment, we need to
// translate that to a point on the time line, so we just add
// it to the "point on time line" value to move to the next point :)
potl += normalised;
timeLine.add(potl, b);
}
currentPoint = path.get(0);
Ticker.INSTANCE.add(this);
}
#Override
public Dimension getPreferredSize() {
Dimension size = pathShape.getBounds().getSize();
size.width += pathShape.getBounds().x;
size.height += pathShape.getBounds().y;
return size;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.GREEN);
g2d.draw(pathShape);
g2d.setColor(Color.RED);
g2d.fill(new Ellipse2D.Double(currentPoint.x - 5, currentPoint.y - 5, 10, 10));
g2d.dispose();
}
#Override
public void tick() {
if (startTime == null) {
startTime = System.currentTimeMillis();
}
long diff = System.currentTimeMillis() - startTime;
double t = (double)diff / PLAY_TIME;
if (t > 1.0) {
t = 1.0d;
// Don't call me any more, I'm already home
Ticker.INSTANCE.remove(this);
}
currentPoint = timeLine.getPointAt(t);
repaint();
}
}
public class Path implements Iterable<Point> {
private List<Point> points;
private double totalLength = 0;
public Path(Point... points) {
this.points = new ArrayList<>(Arrays.asList(points));
for (int index = 0; index < size() - 1; index++) {
Point a = get(index);
Point b = get(index + 1);
double length = lengthBetween(a, b);
totalLength += length;
}
}
public double getTotalLength() {
return totalLength;
}
public int size() {
return points.size();
}
public Point get(int index) {
return points.get(index);
}
public double lengthBetween(Point a, Point b) {
return Math.sqrt(
(a.getX() - b.getX()) * (a.getX() - b.getX())
+ (a.getY() - b.getY()) * (a.getY() - b.getY()));
}
#Override
public Iterator<Point> iterator() {
return points.iterator();
}
}
public class Timeline {
private Map<Double, KeyFrame> mapEvents;
public Timeline() {
mapEvents = new TreeMap<>();
}
public void add(double progress, Point p) {
mapEvents.put(progress, new KeyFrame(progress, p));
}
public Point getPointAt(double progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
KeyFrame[] keyFrames = getKeyFramesBetween(progress);
double max = keyFrames[1].progress - keyFrames[0].progress;
double value = progress - keyFrames[0].progress;
double weight = value / max;
return blend(keyFrames[0].getPoint(), keyFrames[1].getPoint(), 1f - weight);
}
public KeyFrame[] getKeyFramesBetween(double progress) {
KeyFrame[] frames = new KeyFrame[2];
int startAt = 0;
Double[] keyFrames = mapEvents.keySet().toArray(new Double[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 Point blend(Point start, Point end, double ratio) {
Point blend = new Point();
double ir = (float) 1.0 - ratio;
blend.x = (int) (start.x * ratio + end.x * ir);
blend.y = (int) (start.y * ratio + end.y * ir);
return blend;
}
public class KeyFrame {
private double progress;
private Point point;
public KeyFrame(double progress, Point point) {
this.progress = progress;
this.point = point;
}
public double getProgress() {
return progress;
}
public Point getPoint() {
return point;
}
}
}
}
Now, if I was doing this, I would create a method either in Path or as a static utility method, that took a Path and returned a TimeLine automatically ;)
Related
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed last year.
Improve this question
Step 1: Task
Okay so I'm trying to replicate this in a jframe.
https://codepen.io/allanpope/pen/LVWYYd
Problem:
I have a loading screen and over the course of 5 seconds I'm moving dots from a circle to an image. Just like the codepen. Except the problem is I'm not sure how to animate it correctly. JAVA
My Idea was to make an animate function like Move.to(Dot,Duration)
One problem I'm having is that when using decimals the dots wont be in the exact place, and some wont move at all. I'm just not sure how to make this animate function and how to end it. If anyone could help me that would be epic. My code is down below. And if someone could tell me how I could do a Bezier curve animation would also be cool
If anyone wantes to test my code and tell me whats wrong that would be epic.
So first I have a function to make the circle positions
package loadingScreen;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
public class LoadTree {
static BufferedImage tree;
static int radius =300;
public static List<Dot> dots = new ArrayList<Dot>();
public static void make() {
File e = new File("assets/tree.png");
try {
tree = ImageIO.read(e);
} catch (IOException e1) {
e1.printStackTrace();
}
for(int x = 0; x<tree.getWidth();x+=1) {
for(int y = 0; y<tree.getWidth();y+=1) {
int clr = tree.getRGB(x, y);
if(clr==0) {
}else {
int i =(y)*300+x;
int a = (i / 4) % 300;
int b = (int) Math.floor(Math.floor(i / 300) / 4);
if (( a % ((1)) == 0) && (b % ((1)) == 0)) {
double p = (double) Math.random();
int circleX = (int) (MyFrame.width/2+ radius * Math.cos(2 * Math.PI * p));
int circleY = (int) (MyFrame.height/2 + radius * Math.sin(2 * Math.PI * p));
Dot dot = new Dot(circleX, circleY, clr,
MyFrame.width/2+x-150, MyFrame.height/2+y-150,circleX,circleY);
dots.add(dot);
}
}
}
}
}
}
Here is the dot class
package loadingScreen;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
public class Dot {
public int imageY;
public int imageX;
public int color;
public double y;
public double x;
public int circleX;
public int circleY;
public Dot(int x, int y, int color, int imageX, int imageY, int circleX, int circleY){
this.x = x;
this.y = y;
this.color = color;
this.imageX = imageX;
this.imageY = imageY;
this.circleX = circleX;
this.circleY = circleY;
}
public void paint(Graphics g){
Graphics2D g2d = (Graphics2D)g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
Color c = new Color(color);
g2d.setColor(c);
g2d.fillRect((int)x,(int) y, 1, 1);
}
}
Here is the Jpanel
package loadingScreen;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.Timer;
import loadingScreen.animate.Move;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URL;
public class MyPanel extends JPanel implements ActionListener{
Image background;
Timer timer;
MyPanel(){
timer = new Timer(0,this);
timer.start();
File e = new File("assets/background.png");
try {
background = ImageIO.read(e);
} catch (IOException e1) {
e1.printStackTrace();
}
}
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
//g2d.drawImage(background, 0, 0, MyFrame.width,MyFrame.height,null);
LoadTree.dots.forEach(dot ->{
dot.paint(g);
});
}
#Override
public void actionPerformed(ActionEvent e) {
LoadTree.dots.forEach(dot ->{
Move.to(dot, 1000, 0);
//get slope
});
repaint();
//System.out.println(LoadTree.dots.size());
}
}
And here is the Jframe
package loadingScreen;
import javax.swing.JFrame;
public class MyFrame extends JFrame{
MyPanel panel;
public static int width = 1080;
public static int height = 720;
MyFrame(){
panel = new MyPanel();
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.add(panel);
this.setSize(width, height);
this.setLocationRelativeTo(null);
this.setVisible(true);
}
}
You are in for a massive deep dive. Animation, good animation, animation you don't notice, is really, really hard to achieve and is a very complex subject.
You've kind of started in the right direction. You need some kind of "ticker" to tell you when the animation should update, but it kind of falls apart after that.
The first thing you want to do is move away from the concept of "linear progression". That is, on each "tick", the object is moved by a specific delta value. This doesn't produce good animation and can fall apart really quickly when you want to change the speed or duration of the animation.
A better solution is to start with a "duration based progress". This is, basically, the animation will run over a fixed period of time and on each tick of the animation, you calculate the new "state" of the object based on the amount of time which has passed and the amount of time remaining.
This has the benefit of "normalising" the timeline. That is, the animation occurs between 0-1. From this it becomes incredibly easy to calculate where a object should be along that time line. Want to make it faster or slower? Change the duration, the rest is taken care for you!
To start with, figure out how to move one dot from one point to another, if you can move one, you can move a thousand.
Duration base animation engine...
Play close attention to:
The Utilities class
The DurationAnimationEngine
The engine is backed by a Swing Timer, so it's safe to use within Swing. It's whole purpose to run (as fast as it safely can) for a specified period of time and produce "ticks" with the amount of progression has occurred (remember, normalised time)
The following is basic implementation of the animation. A lot of the "work" happens in the mouseClicked event, as it starts the engine. When the engine ticks the dots are updated. Each dot is wrapped in AnimatableDot which has a "from" and "to" point, it then, based on the normalised time, calculates it's new position and then a paint pass is executed
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.Ellipse2D;
import java.awt.geom.Point2D;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class DurationTest {
public static void main(String[] args) {
new DurationTest();
}
public DurationTest() {
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 Utilities {
public static Point2D pointOnCircle(double degress, double radius) {
double rads = Math.toRadians(degress - 90); // 0 becomes the top
double xPosy = Math.round((Math.cos(rads) * radius));
double yPosy = Math.round((Math.sin(rads) * radius));
return new Point2D.Double(radius + xPosy, radius + yPosy);
}
public static Point2D pointOnCircle(double xOffset, double yOffset, double degress, double radius) {
Point2D poc = pointOnCircle(degress, radius);
return new Point2D.Double(xOffset + poc.getX(), yOffset + poc.getY());
}
}
public class TestPane extends JPanel {
private List<AnimatedDot> dots = new ArrayList<>(128);
private Duration duration = Duration.ofSeconds(5);
private DurationAnimationEngine engine;
private List<Color> colors = Arrays.asList(new Color[]{
Color.RED,
Color.BLUE,
Color.CYAN,
Color.GRAY,
Color.GREEN,
Color.LIGHT_GRAY,
Color.MAGENTA,
Color.PINK,
Color.WHITE,
Color.YELLOW
});
public TestPane() {
Random rnd = new Random();
setBackground(Color.BLACK);
for (int index = 0; index < 100; index++) {
double fromAngle = 360.0 * rnd.nextDouble();
double toAngle = fromAngle + 180.0;
Collections.shuffle(colors);
Color color = colors.get(0);
dots.add(new AnimatedDot(
Utilities.pointOnCircle(fromAngle, 150),
Utilities.pointOnCircle(toAngle, 150),
color, 2));
}
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (engine != null) {
engine.stop();
engine = null;
// Reset poisitions
for (AnimatedDot dot : dots) {
dot.move(0);
}
repaint();
return;
}
engine = new DurationAnimationEngine(duration, new DurationAnimationEngine.Tickable() {
#Override
public void animationDidTick(double progress) {
for (AnimatedDot dot : dots) {
dot.move(progress);
}
repaint();
}
});
engine.start();
}
});
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
int xOffset = (getWidth() - 300) / 2;
int yOffset = (getWidth() - 300) / 2;
g2d.translate(xOffset, yOffset);
g2d.setColor(Color.DARK_GRAY);
g2d.drawOval(0, 0, 300, 300);
for (AnimatedDot dot : dots) {
dot.paint(g2d);
}
g2d.dispose();
}
}
public class DurationAnimationEngine {
public interface Tickable {
public void animationDidTick(double progress);
}
private Duration duration;
private Instant timeStarted;
private Timer timer;
private Tickable tickable;
public DurationAnimationEngine(Duration duration, Tickable tickable) {
this.duration = duration;
this.tickable = tickable;
}
public void start() {
// You could create the timer lazierly and restarted it as needed
if (timer != null) {
return;
}
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (timeStarted == null) {
timeStarted = Instant.now();
}
Duration runtime = Duration.between(timeStarted, Instant.now());
double progress = Math.min(1.0, runtime.toMillis() / (double) duration.toMillis());
tickable.animationDidTick(progress);
if (progress >= 1.0) {
stop();
}
}
});
timer.start();
}
public void stop() {
if (timer == null) {
return;
}
timer.stop();
timer = null;
}
}
public class AnimatedDot {
private Dot dot;
private Point2D from;
private Point2D to;
public AnimatedDot(Point2D from, Point2D to, Color color, int radius) {
dot = new Dot(from.getX(), from.getY(), color, radius);
this.from = from;
this.to = to;
}
public void paint(Graphics2D g) {
dot.paint(g);
}
public void move(double progress) {
Point2D pointAt = pointAt(progress);
dot.setLocation(pointAt);
}
public Point2D getFrom() {
return from;
}
public Point2D getTo() {
return to;
}
protected double getFromX() {
return getFrom().getX();
}
protected double getFromY() {
return getFrom().getY();
}
public Double getXDistance() {
return getTo().getX() - getFrom().getX();
}
public Double getYDistance() {
return getTo().getY() - getFrom().getY();
}
protected Point2D pointAt(double progress) {
double xDistance = getXDistance();
double yDistance = getYDistance();
double xValue = Math.round(xDistance * progress);
double yValue = Math.round(yDistance * progress);
xValue += getFromX();
yValue += getFromY();
return new Point2D.Double(xValue, yValue);
}
}
public class Dot {
private Color color;
private double y;
private double x;
private int radius;
private Ellipse2D dot;
public Dot(double x, double y, Color color, int radius) {
this.x = x;
this.y = y;
this.color = color;
this.radius = radius;
dot = new Ellipse2D.Double(0, 0, radius * 2, radius * 2);
}
public void setLocation(Point2D point) {
setLocation(point.getX(), point.getY());
}
public void setLocation(double x, double y) {
this.x = x;
this.y = y;
}
public void paint(Graphics2D g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(color);
g2d.translate(x - radius, y - radius);
g2d.fill(dot);
g2d.dispose();
}
}
}
Okay, so when we run it, we get...
😮 ... Hmmm, I'd like to say that that was expected, but once I saw it, it was obvious what had gone wrong.
All the dots are moving at the same speed over the same time range!
So, what's the answer. Well, actually a few...
We could change the duration of each dot so that they have an individual duration. This would "randomise" the movement, but I'm not sure it would generate the exact same effect, as they'd be moving at different speeds
We could randomise the start time of the dots, so they started at different times, allowing them all to have the same duration (or even a randomised duration)
We could move only a small subset of the dots, but this would mean that you'd probably end up waiting for the current subset to finish before the next one started
A combination of 2 & 3
Individualised, randomised duration...
Okay, for simplicity (and my sanity), I'm actually going to start with 1. Each dot will have it's own, randomised duration. This means that each dot will be moving at a different speed though.
Pay close attention to LinearAnimationEngine and the AnimatedDot#move method.
This should look familiar, it's basically the same animation logic as before, just isolated for the dot itself
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.Ellipse2D;
import java.awt.geom.Point2D;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class RandomIndividualDuration {
public static void main(String[] args) {
new RandomIndividualDuration();
}
public RandomIndividualDuration() {
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 Utilities {
public static Point2D pointOnCircle(double degress, double radius) {
double rads = Math.toRadians(degress - 90); // 0 becomes the top
double xPosy = Math.round((Math.cos(rads) * radius));
double yPosy = Math.round((Math.sin(rads) * radius));
return new Point2D.Double(radius + xPosy, radius + yPosy);
}
public static Point2D pointOnCircle(double xOffset, double yOffset, double degress, double radius) {
Point2D poc = pointOnCircle(degress, radius);
return new Point2D.Double(xOffset + poc.getX(), yOffset + poc.getY());
}
}
public class DurationRange {
private Duration from;
private Duration to;
public DurationRange(Duration from, Duration to) {
this.from = from;
this.to = to;
}
public Duration getFrom() {
return from;
}
public Duration getTo() {
return to;
}
public Duration getDistance() {
return Duration.ofNanos(getTo().toNanos() - getFrom().toNanos());
}
public Duration valueAt(double progress) {
Duration distance = getDistance();
long value = (long) Math.round((double) distance.toNanos() * progress);
value += getFrom().getNano();
return Duration.ofNanos(value);
}
}
public class TestPane extends JPanel {
private List<AnimatedDot> dots = new ArrayList<>(128);
private Duration duration = Duration.ofSeconds(5);
private LinearAnimationEngine engine;
private List<Color> colors = Arrays.asList(new Color[]{
Color.RED,
Color.BLUE,
Color.CYAN,
Color.GRAY,
Color.GREEN,
Color.LIGHT_GRAY,
Color.MAGENTA,
Color.PINK,
Color.WHITE,
Color.YELLOW
});
public TestPane() {
Random rnd = new Random();
setBackground(Color.BLACK);
DurationRange range = new DurationRange(Duration.ofSeconds(1), Duration.ofSeconds(5));
for (int index = 0; index < 100; index++) {
double fromAngle = 360.0 * rnd.nextDouble();
double toAngle = fromAngle + 180.0;
Collections.shuffle(colors);
Color color = colors.get(0);
Duration duration = range.valueAt(rnd.nextDouble());
dots.add(new AnimatedDot(
Utilities.pointOnCircle(fromAngle, 150),
Utilities.pointOnCircle(toAngle, 150),
color, 2, duration));
}
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (engine != null) {
engine.stop();
engine = null;
reset();
return;
}
System.out.println("Go");
List<AnimatedDot> avaliableDots = new ArrayList<>(120);
avaliableDots.addAll(dots);
engine = new LinearAnimationEngine(new LinearAnimationEngine.Tickable() {
#Override
public void animationDidTick() {
List<AnimatedDot> completed = new ArrayList<>(128);
// Reset poisitions
for (AnimatedDot dot : avaliableDots) {
if (!dot.move()) {
completed.add(dot);
}
}
avaliableDots.removeAll(completed);
repaint();
if (avaliableDots.isEmpty()) {
engine.stop();
engine = null;
reset();
}
}
});
engine.start();
}
});
}
protected void reset() {
for (AnimatedDot dot : dots) {
dot.reset();
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(400, 400);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
int xOffset = (getWidth() - 300) / 2;
int yOffset = (getWidth() - 300) / 2;
g2d.translate(xOffset, yOffset);
g2d.setColor(Color.DARK_GRAY);
g2d.drawOval(0, 0, 300, 300);
for (AnimatedDot dot : dots) {
dot.paint(g2d);
}
g2d.dispose();
}
}
public class LinearAnimationEngine {
public interface Tickable {
public void animationDidTick();
}
private Tickable tickable;
private Timer timer;
public LinearAnimationEngine(Tickable tickable) {
this.tickable = tickable;
}
public void start() {
// You could create the timer lazierly and restarted it as needed
if (timer != null) {
return;
}
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
tickable.animationDidTick();
}
});
timer.start();
}
public void stop() {
if (timer == null) {
return;
}
timer.stop();
timer = null;
}
}
public class AnimatedDot {
private Dot dot;
private Point2D from;
private Point2D to;
private Duration duration;
private Instant timeStarted;
public AnimatedDot(Point2D from, Point2D to, Color color, int radius, Duration duration) {
dot = new Dot(from.getX(), from.getY(), color, radius);
this.from = from;
this.to = to;
this.duration = duration;
}
public void paint(Graphics2D g) {
dot.paint(g);
}
public void reset() {
Point2D futureFrom = to;
to = from;
from = futureFrom;
timeStarted = null;
}
public boolean move() {
if (timeStarted == null) {
timeStarted = Instant.now();
}
Duration runtime = Duration.between(timeStarted, Instant.now());
double progress = Math.min(1.0, runtime.toMillis() / (double) duration.toMillis());
Point2D pointAt = pointAt(progress);
dot.setLocation(pointAt);
return progress < 1.0;
}
public Point2D getFrom() {
return from;
}
public Point2D getTo() {
return to;
}
protected double getFromX() {
return getFrom().getX();
}
protected double getFromY() {
return getFrom().getY();
}
public Double getXDistance() {
return getTo().getX() - getFrom().getX();
}
public Double getYDistance() {
return getTo().getY() - getFrom().getY();
}
protected Point2D pointAt(double progress) {
double xDistance = getXDistance();
double yDistance = getYDistance();
double xValue = Math.round(xDistance * progress);
double yValue = Math.round(yDistance * progress);
xValue += getFromX();
yValue += getFromY();
return new Point2D.Double(xValue, yValue);
}
}
public class Dot {
private Color color;
private double y;
private double x;
private int radius;
private Ellipse2D dot;
public Dot(double x, double y, Color color, int radius) {
this.x = x;
this.y = y;
this.color = color;
this.radius = radius;
dot = new Ellipse2D.Double(0, 0, radius * 2, radius * 2);
}
public void setLocation(Point2D point) {
setLocation(point.getX(), point.getY());
}
public void setLocation(double x, double y) {
this.x = x;
this.y = y;
}
public void paint(Graphics2D g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(color);
g2d.translate(x - radius, y - radius);
g2d.fill(dot);
g2d.dispose();
}
}
}
Now, when we run it we get...
Well, at least it's now more "randomised", and this is where I think points 2 & 3 might be a better mix.
But they're not rebounding?!
Ah, well, actually, click the second example again! The dots will move from the current position (the original to point) and back to their original from point. Soooo, conceptually, it's doable.
But they don't from a nice picture when I click it!
😐 ... So the above examples demonstrate how to animate a object from point A to point B over a specified duration, forming the picture is just changing the target destination (assuming you know what it was to start with). Based on my observations, a moving dot is first allowed to move to its current "end" position before moving to the final picture position, as trying to calculate a curving path would make me 🤯.
What's missing...
Yes, there's something missing, you probably can't see it, but it really stands out for me.
Each dot starts out slowly, speeds up and then decelerates into position. This is known as "easement" (or, in this case, ease-in/ease-out) and it's not the simplest thing in the world to implement. If you're really interested, take a look at How can I implement easing functions with a thread
Now, what is the actual answer to your question? Unless you're completely crazy (and I am), don't try to roll this kind of thing yourself, unless you have a very specific reason for doing so. Instead, make use one of the other ready made engines, for example:
universal-tween-engine
timingframework
Much of the concepts used above are taken from my animation playground source, Super Simple Swing Animation Framework. This is where I do a lot of my playing around and experimentation
This is one of those questions that has you digging deeper and trying to figure out what you could actually achieve, to that end BounceImagePixel is an accumulation of much tinkering to see "where this could go"
Here is the task:
Ants move in one place in the region of their residence (for example, [0; 0]) in a straight line with a speed V, and then turn back to the point of their birth with the same speed.I have problems with the moving of objects. The object must stop at the certain point and go back to starting point. How should I fix my code? Some code I have written:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
class vThread extends Thread{
public void run(){
new LabSevenFirst();
System.out.println(Thread.currentThread().getName());
}
}
public class LabSevenFirst extends JPanel implements ActionListener {
private JFrame fr;
double x = 10;
double y = 10;
double r = 10;
public static double T=0, V;
private float x1, y1, x2, y2, xc, yc;
private int t0;
private Timer timer;
private JButton start, stop, apply;
private JLabel forx1, fory1, forx2, fory2, forV;
private JTextField fx1, fy1, fx2, fy2, fV;
public static void main(String[] args) throws InterruptedException {
vThread mt = new vThread();
mt.setName("Ants-labours");
mt.start();
Thread.yield();//позволяет досрочно завершить квант времени текущей нити
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName());
}
LabSevenFirst() {
t0 = 1000/60;
timer = new Timer(t0, this);
timer.setActionCommand("timer");
fr = new JFrame("Movement of ants-labours");
fr.setLayout(null);
fr.setSize(600, 600);
fr.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBounds(100, 50, 300, 300);
start = new JButton("Start");
stop = new JButton("Stop");
apply = new JButton("Apply");
forx1 = new JLabel("x1");
fory1 = new JLabel("y1");
forx2 = new JLabel("x2");
fory2 = new JLabel("y2");
forV = new JLabel("V");
fx1 = new JTextField(x1 + "");
fy1 = new JTextField(y1 + "");
fx2 = new JTextField(x2 + "");
fy2 = new JTextField(y2 + "");
fV = new JTextField(V + "");
forx1.setBounds(5, 380, 20, 20);
fory1.setBounds(5, 400, 20, 20);
forx2.setBounds(5, 420, 20, 20);
fory2.setBounds(5, 440, 20, 20);
forV.setBounds(5, 460, 20, 20);
fx1.setBounds(30, 380, 40, 20);
fy1.setBounds(30, 400, 40, 20);
fx2.setBounds(30, 420, 40, 20);
fy2.setBounds(30, 440, 40, 20);
fV.setBounds(30, 460, 40, 20);
start.setActionCommand("start");
stop.setActionCommand("stop");
apply.setActionCommand("apply");
start.addActionListener(this);
stop.addActionListener(this);
apply.addActionListener(this);
start.setBounds(300, 430, 80, 20);
stop.setBounds(390, 430, 80, 20);
apply.setBounds(210, 430, 80, 20);
fr.add(this);
fr.add(start);
fr.add(stop);
fr.add(apply);
fr.add(forx1);
fr.add(fory1);
fr.add(forx2);
fr.add(fory2);
fr.add(forV);
fr.add(fx1);
fr.add(fy1);
fr.add(fx2);
fr.add(fy2);
fr.add(fV);
fr.setVisible(true);
}
#Override
protected void paintComponent(Graphics g) {
int width = getWidth();
int height = getHeight();
//System.out.println("width" + width);
// System.out.println("height" + height);
g.setColor(Color.yellow);
g.fillRect(0, 0, width, height);
Graphics2D g2d = (Graphics2D) g;
g2d.setStroke(new BasicStroke(3f));
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//double x = 0.5 * width;
//double y = 0.5 * height;
double r = 0.75 * Math.min(x, y);
double dx,dy;
double t,l;
l=Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));
// System.out.println("!!l!!"+l);
t= l/V;
//System.out.println("!!t!!"+t);
g2d.setColor(Color.black);
if(T<t) {
dx = ((x2 - x1) / (Math.sqrt(Math.pow(x2 - x1, 2)) + Math.pow(y2 - y1, 2)));
//System.out.println("!!dx!!" + dx);
dy = ((y2 - y1) / (Math.sqrt(Math.pow(x2 - x1, 2)) + Math.pow(y2 - y1, 2)));
//System.out.println("!!dy!!" + dy);
x += x1 + dx * V * T;//+ dx * (V * T);
//System.out.println("!!x!!" + x);
//System.out.println("!!x1!!" + x1);
y += y1 + dy * V * T;// + dy * (V * T);
r = Math.max(0.1 * r, 5);
// System.out.println("!!y!!" + y);
//System.out.println("!!y1!!" + x1);
}
if (x==x2 && y == y2 && T>t) {
dx = ((x2 - x1) / (Math.sqrt(Math.pow(x2 - x1, 2)) + Math.pow(y2 - y1, 2)));
dy = ((y2 - y1) / (Math.sqrt(Math.pow(x2 - x1, 2)) + Math.pow(y2 - y1, 2)));
x -= x1 + dx * V * T;//+ dx * (V * T);
y -= y1 + dy * V * T;// + dy * (V * T);
r = Math.max(0.1 * r, 5);
}
g2d.fill(circle(x,y,r));
//if (x == x2 && y == y2)
// x = x1 -
}
public Shape circle(double x, double y, double R){
return new Ellipse2D.Double(x - r, y - r, 2 * r, 2 * r);
}
#Override
public void actionPerformed(ActionEvent e) {
switch (e.getActionCommand()) {
case "stop": {
timer.stop();
break;
}
case "start": {
timer.start();
break;
}
case "apply": {
float ax1, ay1, bx2, by2, cv;
try {
ax1 = Float.parseFloat(fx1.getText());
ay1 = Float.parseFloat(fy1.getText());
bx2 = Float.parseFloat(fx2.getText());
by2 = Float.parseFloat(fy2.getText());
cv = Float.parseFloat(fV.getText());
x1 = ax1;
y1 = ay1;
x2 = bx2;
y2 = by2;
V = cv;
repaint();
} catch (NumberFormatException ex) {
JOptionPane.showMessageDialog(null, "Invalid input", "Error",
JOptionPane.ERROR_MESSAGE);
}
break;
}
case "timer": {
T += 0.6;
System.out.println("!!T!!"+T);
repaint();
break;
}
}
}
}
The OP defined a task:
Ants move in one place in the region of their residence (for example,
[0; 0]) in a straight line with a speed V, and then turn back to the
point of their birth with the same speed.I have problems with the
moving of objects. The object must stop at the certain point and go
back to starting point.
And then he asked?
How should I fix my code?
It's too late. There's too many lines of code to debug and test.
So let's start over.
Here's the first iteration of the new code.
import javax.swing.SwingUtilities;
public class MovingAnts implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new MovingAnts());
}
public MovingAnts() {
}
#Override
public void run() {
// TODO Auto-generated method stub
}
}
We can test this code by running it and observing that it does not abend.
So, let's add a bit more code. We know we're going to have to define one or more ants. So, let's create an Ant class.
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import javax.swing.SwingUtilities;
public class MovingAnts implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new MovingAnts());
}
private List<Ant> ants;
public MovingAnts() {
ants = new ArrayList<>();
Point origin = new Point(10, 10);
Point destination = new Point(200, 300);
Ant ant = new Ant(5.0d, origin, destination);
ants.add(ant);
}
#Override
public void run() {
// TODO Auto-generated method stub
}
public class Ant {
private final double velocity;
private Point position;
private final Point startPosition;
private final Point endPosition;
public Ant(double velocity, Point startPosition,
Point endPosition) {
this.velocity = velocity;
this.startPosition = startPosition;
this.endPosition = endPosition;
}
public double getVelocity() {
return velocity;
}
public Point getPosition() {
return position;
}
public void setPosition(Point position) {
this.position = position;
}
public Point getStartPosition() {
return startPosition;
}
public Point getEndPosition() {
return endPosition;
}
}
}
We've defined a velocity (speed), a starting position, and an ending position. According to the task description, these values don't change, so we can mark them final and define them in the constructor.
We've also defined a current position. The current position will be important later when it's time to draw the ant on a drawing JPanel.
We will probably add more to the Ant class as we develop more code. But for now, we have a class that holds the important variables for a ant.
We defined an ant (one instance of the Ant class) and saved the ant in a List<Ant> in the MovingAnts constructor. We can define more later, but let's start with one ant.
Now, we can create the JFrame and drawing JPanel for the ants.
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class MovingAnts implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new MovingAnts());
}
private Dimension drawingPanelSize;
private DrawingPanel drawingPanel;
private List<Ant> ants;
public MovingAnts() {
drawingPanelSize = new Dimension(400, 400);
ants = new ArrayList<>();
Point origin = new Point(10, 10);
Point destination = new Point(200, 300);
Ant ant = new Ant(5.0d, origin, destination);
ants.add(ant);
}
#Override
public void run() {
JFrame frame = new JFrame("Moving Ants");
frame.setDefaultCloseOperation(
JFrame.EXIT_ON_CLOSE);
drawingPanel = new DrawingPanel(
drawingPanelSize);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
public DrawingPanel(Dimension drawingPanelSize) {
this.setPreferredSize(drawingPanelSize);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
}
}
public class Ant {
private final double velocity;
private Point position;
private final Point startPosition;
private final Point endPosition;
public Ant(double velocity, Point startPosition,
Point endPosition) {
this.velocity = velocity;
this.startPosition = startPosition;
this.endPosition = endPosition;
}
public double getVelocity() {
return velocity;
}
public Point getPosition() {
return position;
}
public void setPosition(Point position) {
this.position = position;
}
public Point getStartPosition() {
return startPosition;
}
public Point getEndPosition() {
return endPosition;
}
}
}
Notice how every method and class is short and to the point. No person can read and understand hundreds of lines of code in a single method.
We've added a little bit of code at a time and tested each bit of code by running the application. At his point, we have a GUI. We also don't have any abends. Both the GUI and the lack of abends are important.
We defined the size of the drawing panel. This is important. We don't care how big the JFrame is. We care how big the drawing JPanel is, so we can keep the ants within the bounds of the drawing panel.
We haven't put any code in the paintComponent method of the drawing panel yet. Before we can do that, we have to create an Animation class that will update the position of the ants.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class MovingAnts implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new MovingAnts());
}
private Animation animation;
private Dimension drawingPanelSize;
private DrawingPanel drawingPanel;
private List<Ant> ants;
public MovingAnts() {
drawingPanelSize = new Dimension(400, 400);
ants = new ArrayList<>();
Point origin = new Point(200, 200);
Point destination = new Point(300, 350);
Ant ant = new Ant(30.0d, origin, destination);
ants.add(ant);
}
#Override
public void run() {
JFrame frame = new JFrame("Moving Ants");
frame.setDefaultCloseOperation(
JFrame.EXIT_ON_CLOSE);
drawingPanel = new DrawingPanel(
drawingPanelSize);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
animation = new Animation();
new Thread(animation).start();
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
public DrawingPanel(Dimension drawingPanelSize) {
this.setPreferredSize(drawingPanelSize);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.BLACK);
for (Ant ant : ants) {
Point position = ant.getPosition();
g.fillOval(position.x - 4,
position.y - 4, 8, 8);
}
}
}
public class Animation implements Runnable {
private volatile boolean running;
public Animation() {
this.running = true;
}
#Override
public void run() {
int fps = 20;
long delay = 1000L / fps;
while (running) {
calculateAntPosition(fps);
updateDrawingPanel();
sleep(delay);
}
}
private void calculateAntPosition(int fps) {
for (Ant ant : ants) {
ant.calculatePosition(fps);
// System.out.println(ant.getPosition());
}
}
private void updateDrawingPanel() {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
drawingPanel.repaint();
}
});
}
private void sleep(long duration) {
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
// Deliberately left empty
}
}
public synchronized void setRunning(
boolean running) {
this.running = running;
}
}
public class Ant {
private boolean returning;
private double totalDistance;
private double traveledDistance;
private double theta;
private final double velocity;
private Point position;
private final Point startPosition;
private final Point endPosition;
public Ant(double velocity, Point startPosition,
Point endPosition) {
this.velocity = velocity;
this.startPosition = startPosition;
this.position = startPosition;
this.endPosition = endPosition;
this.returning = false;
this.theta = calculateTheta();
this.totalDistance = calculateTotalDistance();
this.traveledDistance = 0d;
}
private double calculateTheta() {
return Math.atan2((endPosition.y - startPosition.y),
endPosition.x - startPosition.x);
}
private double calculateTotalDistance() {
double diffX = endPosition.x - startPosition.x;
double diffY = endPosition.y - startPosition.y;
return Math.sqrt((diffX * diffX) + (diffY * diffY));
}
public double getVelocity() {
return velocity;
}
public Point getPosition() {
return position;
}
public void calculatePosition(int fps) {
double distance = velocity / fps;
double angle = theta;
if (returning) {
angle += Math.PI;
}
int x = (int) Math.round(
position.x + distance * Math.cos(angle));
int y = (int) Math.round(
position.y + distance * Math.sin(angle));
traveledDistance += distance;
if (traveledDistance > totalDistance) {
returning = !returning;
traveledDistance = 0d;
}
this.position = new Point(x, y);
}
public Point getStartPosition() {
return startPosition;
}
public Point getEndPosition() {
return endPosition;
}
}
}
I added way too much code this iteration, but we now have an ant that walks back and forth between two points.
The Animation class is a Runnable that runs in a Thread. You could use a Swing Timer, but it's easier for me to create the Runnable.
The Ant class grew some chest hair. All the trigonomic calculations can be found in the Ant class. Basically, I used polar coordinates to calculate the position of the ant.
The paintComponent method of the drawing panel simply draws the ants.
Every method and class is small and hopefully, easy to understand. Write short methods. Write short classes.
Hopefully, this code will provide a solid base for you to expand your project.
So I'm building this music player app which plays notes which are dragged and dropped onto a JLabel. When I hit the play button, I want each note to be highlighted with a delay value corresponding to that note. I used a Swing Timer for this but the problem is, it just loops with a constant delay which is specified in the constructor.
playButton.addActionListener(e -> {
timerI = 0;
System.out.println("Entered onAction");
Timer t = new Timer(1000, e1 -> {
if (timerI < 24) {
NoteLabel thisNote = (NoteLabel)staff.getComponent(timerI);
NoteIcon thisIcon = thisNote.getIcon();
String noteName = thisIcon.getNoteName();
thisNote.setIcon(noteMap.get(noteName + "S"));
timerI++;
}
});
t.start();
});
It works and all, but I want to make the timer delay dynamic. Each NoteIcon object has an attribute which holds a delay value and I want the timer to wait a different amount of time depending on which NoteIcon is fetched in that loop. (For exait 1 sec for the first loop, then 2, 4, 1 etc)
How do I do this?
Caveats:
Animation is NOT simple. It's complicated. It has a number of important theories around it which are designed to make animation look good
Good animation is hard
Animation is the illusion of change over time
Much of what I'm presenting is based on library code, so it will be slightly convoluted, but is designed for re-use and abstraction
Theory tl;dr
Okay, some really boring theory. But first, things I'm not going to talk about - easement or animation curves. These change the speed at which animation is played over a given period of time, making the animation look more natural, but I could spend the entire answer talking about nothing else :/
The first thing you want to do is abstract your concepts. For example. Animation is typically a change over time (some animation is linear over an infinite amount of time, but, let's try and keep it within the confines of the question).
So immediately, we have two important concepts. The first is duration, the second is the normalised progress from point A to point B over that duration. That is, at half the duration, the progression will be 0.5. This is important, as it allows us to abstract the concepts and make the framework dynamic.
Animation too fast? Change the duration and everything else remains unchanged.
A timeline...
Okay, music is a timeline. It has a defined start and end point (again, keep it simple) and events along that timeline which "do stuff", independent of the music timeline (ie, each note can play for a specified duration, independent of the music timeline, which will have moved on or even finished)
First, we need a note...
public class Note {
private Duration duration;
public Note(Duration duration) {
this.duration = duration;
}
public Duration getDuration() {
return duration;
}
}
And a "event" based timeline, which describes when those notes should be played over a normalised period of time.
public static class EventTimeLine<T> {
private Map<Double, KeyFrame<T>> mapEvents;
public EventTimeLine() {
mapEvents = new TreeMap<>();
}
public void add(double progress, T value) {
mapEvents.put(progress, new KeyFrame<T>(progress, value));
}
public List<T> getValues() {
return Collections.unmodifiableList(mapEvents.values().stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList()));
}
public double getPointOnTimeLineFor(T value) {
for (Map.Entry<Double, KeyFrame<T>> entry : mapEvents.entrySet()) {
if (entry.getValue().getValue() == value) {
return entry.getKey();
}
}
return -1;
}
public List<T> getValuesAt(double progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
return getKeyFramesBetween(progress, 0.01f)
.stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList());
}
public List<KeyFrame<T>> getKeyFramesBetween(double progress, double delta) {
int startAt = 0;
List<Double> keyFrames = new ArrayList<>(mapEvents.keySet());
while (startAt < keyFrames.size() && keyFrames.get(startAt) <= progress - delta) {
startAt++;
}
startAt = Math.min(keyFrames.size() - 1, startAt);
int endAt = startAt;
while (endAt < keyFrames.size() && keyFrames.get(endAt) <= progress + delta) {
endAt++;
}
endAt = Math.min(keyFrames.size() - 1, endAt);
List<KeyFrame<T>> frames = new ArrayList<>(5);
for (int index = startAt; index <= endAt; index++) {
KeyFrame<T> keyFrame = mapEvents.get(keyFrames.get(index));
if (keyFrame.getProgress() >= progress - delta
&& keyFrame.getProgress() <= progress + delta) {
frames.add(keyFrame);
}
}
return frames;
}
public class KeyFrame<T> {
private double progress;
private T value;
public KeyFrame(double progress, T value) {
this.progress = progress;
this.value = value;
}
public double getProgress() {
return progress;
}
public T getValue() {
return value;
}
#Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
Then you could create a music timeline something like...
musicTimeLine = new EventTimeLine<Note>();
musicTimeLine.add(0.1f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.12f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.2f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.21f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.22f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.25f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.4f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.5f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.7f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.8f, new Note(Duration.ofMillis(2000)));
Note, here I've defined the notes as running at a fixed duration. You "could" make them play as a percentage of the duration of the timeline ... but just saying that is hard, so I'll leave that up to you ;)
Animation Engine
The presented (simple) animation engine, uses a single Timer, running at high speed, as a central "tick" engine.
It then notifies Animatable objects which actually perform the underlying animation.
Normally, I animated over a range of values (from - to), but in this case, we're actually only interested in the amount of time that the animation has played. From that we can determine what notes should be getting played AND animate the notes, in the case of this example, change the alpha value, but you could equally change the size of the objects representing the notes, but that would be a different Animatable implementation, which I've not presented here.
If you're interested, my SuperSimpleSwingAnimationFramework, which this example is loosely based on, contains "range" based Animatables ... fun stuff.
In the example, an Animatable is used to drive the music EventTimeLine, which simply checks the timeline for any "notes" which need to be played at the specific point in time.
A second BlendingTimeLine is used to control the alpha value (0-1-0). Each note is then provided with it's own Animatable which drives this blending timeline, and uses its values to animate the change in the alpha of the highlighted note.
This is a great example of the decoupled nature of the API - the BlendingTimeLine is used for ALL the notes. The Animatables simply take the amount of time they have played and extract the required value from the timeline and apply it.
This means that each note is only highlighted as long as its own duration specifies, all independently.
Runnable Example...
nb: If I was doing this, I'd have abstracted the solution to a much higher level
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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
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 EventTimeLine<Note> musicTimeLine;
private DefaultDurationAnimatable timeLineAnimatable;
private Double playProgress;
private Set<Note> playing = new HashSet<Note>(5);
private Map<Note, Double> noteAlpha = new HashMap<>(5);
private DoubleBlender blender = new DoubleBlender();
private BlendingTimeLine<Double> alphaTimeLine = new BlendingTimeLine<>(blender);
public TestPane() {
musicTimeLine = new EventTimeLine<Note>();
musicTimeLine.add(0.1f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.12f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.2f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.21f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.22f, new Note(Duration.ofMillis(500)));
musicTimeLine.add(0.25f, new Note(Duration.ofMillis(1000)));
musicTimeLine.add(0.4f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.5f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.7f, new Note(Duration.ofMillis(2000)));
musicTimeLine.add(0.8f, new Note(Duration.ofMillis(2000)));
alphaTimeLine.add(0.0f, 0.0);
alphaTimeLine.add(0.5f, 1.0);
alphaTimeLine.add(1.0f, 0.0);
timeLineAnimatable = new DefaultDurationAnimatable(Duration.ofSeconds(10),
new AnimatableListener() {
#Override
public void animationChanged(Animatable animator) {
double progress = timeLineAnimatable.getPlayedDuration();
playProgress = progress;
List<Note> notes = musicTimeLine.getValuesAt(progress);
if (notes.size() > 0) {
System.out.println(">> " + progress + " # " + notes.size());
for (Note note : notes) {
playNote(note);
}
}
repaint();
}
}, null);
timeLineAnimatable.start();
}
protected void playNote(Note note) {
// Note is already playing...
// Equally, we could maintain a reference to the animator, mapped to
// the note, but what ever...
if (playing.contains(note)) {
return;
}
playing.add(note);
DurationAnimatable noteAnimatable = new DefaultDurationAnimatable(note.getDuration(), new AnimatableListener() {
#Override
public void animationChanged(Animatable animator) {
DurationAnimatable da = (DurationAnimatable) animator;
double progress = da.getPlayedDuration();
double alpha = alphaTimeLine.getValueAt((float) progress);
noteAlpha.put(note, alpha);
repaint();
}
}, new AnimatableLifeCycleListenerAdapter() {
#Override
public void animationCompleted(Animatable animator) {
playing.remove(note);
noteAlpha.remove(note);
repaint();
}
});
noteAnimatable.start();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 100);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
int startX = 10;
int endX = getWidth() - 10;
int range = endX - startX;
int yPos = getHeight() / 2;
g2d.setColor(Color.DARK_GRAY);
g2d.drawLine(startX, yPos, endX, yPos);
List<Note> notes = musicTimeLine.getValues();
for (Note note : notes) {
double potl = musicTimeLine.getPointOnTimeLineFor(note);
double xPos = startX + (range * potl);
// Technically, this could be cached...
Ellipse2D notePoint = new Ellipse2D.Double(xPos - 2.5, yPos - 2.5, 5, 5);
g2d.fill(notePoint);
if (noteAlpha.containsKey(note)) {
double alpha = noteAlpha.get(note);
// I'm lazy :/
// It's just simpler to copy the current context, modify the
// composite, paint and then dispose of, then trying to
// track and reset the composite manually
Graphics2D alpha2d = (Graphics2D) g2d.create();
alpha2d.setComposite(AlphaComposite.SrcOver.derive((float) alpha));
Ellipse2D playedNote = new Ellipse2D.Double(xPos - 5, yPos - 5, 10, 10);
alpha2d.setColor(Color.RED);
alpha2d.fill(playedNote);
alpha2d.dispose();
}
}
double playXPos = startX + (range * playProgress);
g2d.setColor(Color.RED);
Line2D playLine = new Line2D.Double(playXPos, 0, playXPos, getHeight());
g2d.draw(playLine);
g2d.dispose();
}
}
public class Note {
private Duration duration;
public Note(Duration duration) {
this.duration = duration;
}
public Duration getDuration() {
return duration;
}
}
public static class EventTimeLine<T> {
private Map<Double, KeyFrame<T>> mapEvents;
public EventTimeLine() {
mapEvents = new TreeMap<>();
}
public void add(double progress, T value) {
mapEvents.put(progress, new KeyFrame<T>(progress, value));
}
public List<T> getValues() {
return Collections.unmodifiableList(mapEvents.values().stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList()));
}
public double getPointOnTimeLineFor(T value) {
for (Map.Entry<Double, KeyFrame<T>> entry : mapEvents.entrySet()) {
if (entry.getValue().getValue() == value) {
return entry.getKey();
}
}
return -1;
}
public List<T> getValuesAt(double progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
return getKeyFramesBetween(progress, 0.01f)
.stream()
.map(kf -> kf.getValue())
.collect(Collectors.toList());
}
public List<KeyFrame<T>> getKeyFramesBetween(double progress, double delta) {
int startAt = 0;
List<Double> keyFrames = new ArrayList<>(mapEvents.keySet());
while (startAt < keyFrames.size() && keyFrames.get(startAt) <= progress - delta) {
startAt++;
}
startAt = Math.min(keyFrames.size() - 1, startAt);
int endAt = startAt;
while (endAt < keyFrames.size() && keyFrames.get(endAt) <= progress + delta) {
endAt++;
}
endAt = Math.min(keyFrames.size() - 1, endAt);
List<KeyFrame<T>> frames = new ArrayList<>(5);
for (int index = startAt; index <= endAt; index++) {
KeyFrame<T> keyFrame = mapEvents.get(keyFrames.get(index));
if (keyFrame.getProgress() >= progress - delta
&& keyFrame.getProgress() <= progress + delta) {
frames.add(keyFrame);
}
}
return frames;
}
public class KeyFrame<T> {
private double progress;
private T value;
public KeyFrame(double progress, T value) {
this.progress = progress;
this.value = value;
}
public double getProgress() {
return progress;
}
public T getValue() {
return value;
}
#Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
public static class BlendingTimeLine<T> {
private Map<Float, KeyFrame<T>> mapEvents;
private Blender<T> blender;
public BlendingTimeLine(Blender<T> blender) {
mapEvents = new TreeMap<>();
this.blender = blender;
}
public void setBlender(Blender<T> blender) {
this.blender = blender;
}
public Blender<T> getBlender() {
return blender;
}
public void add(float progress, T value) {
mapEvents.put(progress, new KeyFrame<T>(progress, value));
}
public T getValueAt(float progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
List<KeyFrame<T>> keyFrames = getKeyFramesBetween(progress);
float max = keyFrames.get(1).progress - keyFrames.get(0).progress;
float value = progress - keyFrames.get(0).progress;
float weight = value / max;
T blend = blend(keyFrames.get(0).getValue(), keyFrames.get(1).getValue(), 1f - weight);
return blend;
}
public List<KeyFrame<T>> getKeyFramesBetween(float progress) {
List<KeyFrame<T>> frames = new ArrayList<>(2);
int startAt = 0;
Float[] keyFrames = mapEvents.keySet().toArray(new Float[mapEvents.size()]);
while (startAt < keyFrames.length && keyFrames[startAt] <= progress) {
startAt++;
}
startAt = Math.min(startAt, keyFrames.length - 1);
frames.add(mapEvents.get(keyFrames[startAt - 1]));
frames.add(mapEvents.get(keyFrames[startAt]));
return frames;
}
protected T blend(T start, T end, float ratio) {
return blender.blend(start, end, ratio);
}
public static interface Blender<T> {
public T blend(T start, T end, float ratio);
}
public class KeyFrame<T> {
private float progress;
private T value;
public KeyFrame(float progress, T value) {
this.progress = progress;
this.value = value;
}
public float getProgress() {
return progress;
}
public T getValue() {
return value;
}
#Override
public String toString() {
return "KeyFrame progress = " + getProgress() + "; value = " + getValue();
}
}
}
public class DoubleBlender implements BlendingTimeLine.Blender<Double> {
#Override
public Double blend(Double start, Double end, float ratio) {
double ir = (double) 1.0 - ratio;
return (double) (start * ratio + end * ir);
}
}
public enum Animator {
INSTANCE;
private Timer timer;
private List<Animatable> properies;
private Animator() {
properies = new ArrayList<>(5);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
List<Animatable> copy = new ArrayList<>(properies);
Iterator<Animatable> it = copy.iterator();
while (it.hasNext()) {
Animatable ap = it.next();
ap.tick();
}
if (properies.isEmpty()) {
timer.stop();
}
}
});
}
public void add(Animatable ap) {
properies.add(ap);
timer.start();
}
protected void removeAll(List<Animatable> completed) {
properies.removeAll(completed);
}
public void remove(Animatable ap) {
properies.remove(ap);
if (properies.isEmpty()) {
timer.stop();
}
}
}
// Reprepresents a linear animation
public interface Animatable {
public void tick();
public void start();
public void stop();
}
public interface DurationAnimatable extends Animatable {
public Duration getDuration();
public Double getPlayedDuration();
}
public abstract class AbstractAnimatable implements Animatable {
private AnimatableListener animatableListener;
private AnimatableLifeCycleListener lifeCycleListener;
public AbstractAnimatable(AnimatableListener listener) {
this(listener, null);
}
public AbstractAnimatable(AnimatableListener listener, AnimatableLifeCycleListener lifeCycleListener) {
this.animatableListener = listener;
this.lifeCycleListener = lifeCycleListener;
}
public AnimatableLifeCycleListener getLifeCycleListener() {
return lifeCycleListener;
}
public AnimatableListener getAnimatableListener() {
return animatableListener;
}
#Override
public void tick() {
fireAnimationChanged();
}
#Override
public void start() {
fireAnimationStarted();
Animator.INSTANCE.add(this);
}
#Override
public void stop() {
fireAnimationStopped();
Animator.INSTANCE.remove(this);
}
protected void fireAnimationChanged() {
if (animatableListener == null) {
return;
}
animatableListener.animationChanged(this);
}
protected void fireAnimationStarted() {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationStarted(this);
}
protected void fireAnimationStopped() {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationStopped(this);
}
}
public interface AnimatableListener {
public void animationChanged(Animatable animator);
}
public interface AnimatableLifeCycleListener {
public void animationCompleted(Animatable animator);
public void animationStarted(Animatable animator);
public void animationPaused(Animatable animator);
public void animationStopped(Animatable animator);
}
public class AnimatableLifeCycleListenerAdapter implements AnimatableLifeCycleListener {
#Override
public void animationCompleted(Animatable animator) {
}
#Override
public void animationStarted(Animatable animator) {
}
#Override
public void animationPaused(Animatable animator) {
}
#Override
public void animationStopped(Animatable animator) {
}
}
public class DefaultDurationAnimatable extends AbstractAnimatable implements DurationAnimatable {
private Duration duration;
private Instant startTime;
public DefaultDurationAnimatable(Duration duration, AnimatableListener listener, AnimatableLifeCycleListener lifeCycleListener) {
super(listener, lifeCycleListener);
this.duration = duration;
}
#Override
public Duration getDuration() {
return duration;
}
#Override
public Double getPlayedDuration() {
if (startTime == null) {
return 0.0;
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startTime, Instant.now());
double progress = (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
#Override
public void tick() {
if (startTime == null) {
startTime = Instant.now();
fireAnimationStarted();
}
fireAnimationChanged();
if (getPlayedDuration() >= 1.0) {
fireAnimationCompleted();
stop();
}
}
protected void fireAnimationCompleted() {
AnimatableLifeCycleListener lifeCycleListener = getLifeCycleListener();
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationCompleted(this);
}
}
}
Yes it "seems" complicated, yes it "seems" hard. But when you've done this kind of thing a few times, it becomes simpler and the solution makes a lot more sense.
It's decoupled. It's re-usable. It's flexible.
In this example, I've mostly used paintComponent as the main rendering engine. But you could just as easily use individual components linked together with some kind of event driven framework.
I have a program that does an animation using timers switching images. When the program is on its last image I use a class to create a buffered image of that image with text over it. When the last image of the animation is displayed I want to change the image displayed to the buffered image. I can't get it to work. The code as is plays as if the bolded section isnt there. If I delete the line above it, it displays the image with text over it and nothing else. What edits should I make to my code to fix this?
The Class that does the animation
**import java.awt.event.*;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.image.*;
import java.io.*;
import java.io.File;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.net.URL;
import javax.swing.*;
import javax.swing.*;
import javax.imageio.ImageIO;
/**
* Write a description of class Reveal here.
*
* #author (your name)
* #version (a version number or a date)
*/
public class Reveal extends JPanel
{
private JPanel panel = new JPanel(); //a panel to house the label
private JLabel label = new JLabel(); //a label to house the image
private String[] image = {"Jack in the Box 1.png","Jack in the Box 2.png","Jack in the Box 3.png","Jack in the Box 4.png","Jack in the Box 5.png","Jack in the Box 6.png","Jack in the Box 7.png"}; //an array to hold the frames of the animation
private ImageIcon[] icon = new ImageIcon[7]; //an array of icons to be the images
private JFrame f;
private TextOverlay TO;
private Timer timer;
private Timer timer2;
int x = 0;
int y = 4;
int counter = 0;
/**
* Constructor for objects of class Reveal
*/
public Reveal(String name, int number)
{
TO = new TextOverlay("Jack in the Box 7.png", name, number);
for (int h = 0; h < 7; h++){
icon[h] = new ImageIcon(image[h]);
icon[h].getImage();
}
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
//Sets the size of the window
f.setSize(800,850);
panel = new JPanel();
label = new JLabel();
label.setIcon( icon[x] );
panel.add(label);
setVisible(true);
f.add(panel);
display(name, number);
**f.add(TO);**
}
public void display(String name, int number){
timer = new Timer(150, new ActionListener(){
public void actionPerformed(ActionEvent e) {
if (counter > 27){
timer.stop();
timer2.start(); //starts the second half of the animation
}else{
if (x != 3){
x++;
}else{
x = 0;
}
label.setIcon( icon[x] );
counter++;
} //ends if-else
} //ends action method
}); //ends timer
timer2 = new Timer(250, new ActionListener(){
public void actionPerformed(ActionEvent e){
if (y > 6) {
timer2.stop();
}else{
label.setIcon( icon[y] );
y++;
} //ends if-else
} //ends action method
}); //ends timer2
timer.start();
}
}
**
The class that puts text over an image
import java.io.*;
import java.awt.*;
import javax.swing.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
/**
* #see https://stackoverflow.com/questions/2658663
*/
public class TextOverlay extends JPanel {
private BufferedImage image;
private String name;
private String fileX;
private int number;
public TextOverlay(String f, String s, int n) {
name = s;
number = n;
fileX = f;
try {
image = ImageIO.read(new File(fileX));
} catch (IOException e) {
e.printStackTrace();
}
image = process(image, name, number);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0, 0, null);
}
private BufferedImage process(BufferedImage old, String name, int number) {
int w = old.getWidth();
int h = old.getHeight();
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.drawImage(old, 0, 0, w, h, this);
g2d.setPaint(Color.black);
g2d.setFont(new Font("Franklin Gothic Demi Cond", Font.PLAIN, 30));
String s1 = name;
String s2 = Integer.toString(number);;
FontMetrics fm = g2d.getFontMetrics();
g2d.drawString(s1, 40, 90);
g2d.drawString(s2, 40, 140);
g2d.dispose();
return img;
}
}
So, you seem to have a misunderstanding of how Swing works, you might find How to Use Swing Timers and Concurrency in Swing of some assistance.
Basically, when you start a Timer, it doesn't block at this point until the timer ends (and even if it did, your wouldn't work the way you wanted it to). Instead, a new thread is created and after the specified period a request is placed on Event Dispatching Thread to execute the supplied Runnable.
This means that when you do something like...
f.add(panel);
display(name, number);
f.add(TO);
You are actually adding the TO component onto of the JLabel (because the frame is using a BorderLayout and the CENTRE position is the default position.
Instead, in your second timer completes, you need to remove the label and add the TO component...
timer2 = new Timer(250, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (y > 6) {
timer2.stop();
Container parent = label.getParent();
parent.remove(label);
parent.add(TO);
parent.revalidate();
} else {
label.setIcon(icon[y]);
y++;
} //ends if-else
} //ends action method
}); //ends timer2
Runnable Example...
import java.awt.event.*;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.LineBorder;
public class Reveal extends JPanel {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
new Reveal("Test", 5);
}
});
}
private JPanel panel = new JPanel(); //a panel to house the label
private JLabel label = new JLabel(); //a label to house the image
private ImageIcon[] icon = new ImageIcon[7]; //an array of icons to be the images
private JFrame f;
private TextOverlay TO;
private Timer timer;
private Timer timer2;
int x = 0;
int y = 4;
int counter = 0;
/**
* Constructor for objects of class Reveal
*/
public Reveal(String name, int number) {
TO = new TextOverlay("Jack in the Box 7.png", name, number);
for (int h = 0; h < 7; h++) {
icon[h] = new ImageIcon(makeImage(h));
icon[h].getImage();
}
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
//Sets the size of the window
f.setSize(800, 850);
panel = new JPanel(new GridBagLayout());
label = new JLabel();
label.setIcon(icon[x]);
label.setBorder(new LineBorder(Color.RED));
panel.add(label);
f.add(panel);
display(name, number);
// f.add(TO);
setVisible(true);
}
public void display(String name, int number) {
timer = new Timer(150, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (counter > 27) {
timer.stop();
timer2.start(); //starts the second half of the animation
} else {
if (x != 3) {
x++;
} else {
x = 0;
}
label.setIcon(icon[x]);
counter++;
} //ends if-else
} //ends action method
}); //ends timer
timer2 = new Timer(250, new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (y > 6) {
timer2.stop();
Container parent = label.getParent();
parent.remove(label);
parent.add(TO);
parent.revalidate();
} else {
label.setIcon(icon[y]);
y++;
} //ends if-else
} //ends action method
}); //ends timer2
timer.start();
}
protected BufferedImage makeImage(int h) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
FontMetrics fm = g2d.getFontMetrics();
String text = Integer.toString(h);
int x = (100 - fm.stringWidth(text)) / 2;
int y = ((100 - fm.getHeight()) / 2) + fm.getAscent();
g2d.setColor(Color.BLUE);
g2d.fillRect(0, 0, 100, 100);
g2d.setColor(Color.BLACK);
g2d.drawString(text, x, y);
g2d.dispose();
return img;
}
public class TextOverlay extends JPanel {
private BufferedImage image;
private String name;
private String fileX;
private int number;
public TextOverlay(String f, String s, int n) {
name = s;
number = n;
fileX = f;
image = makeImage(n);
image = process(image, name, number);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0, 0, this);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}
private BufferedImage process(BufferedImage old, String name, int number) {
int w = old.getWidth();
int h = old.getHeight();
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
g2d.drawImage(old, 0, 0, w, h, this);
g2d.setPaint(Color.black);
g2d.setFont(new Font("Franklin Gothic Demi Cond", Font.PLAIN, 30));
String s1 = name;
String s2 = Integer.toString(number);;
FontMetrics fm = g2d.getFontMetrics();
g2d.drawString(s1, 40, 90);
g2d.drawString(s2, 40, 140);
g2d.dispose();
return img;
}
}
}
A "slightly" different approach...
Animation is actually a really complex subject which is not easy to implement well.
This is why, when faced with problems like these, I prefer to look at libraries which have already been implemented to help solve them. I'd recommend having a look at:
The Timing Framework
Trident
universal-tween-engine
as some starting points.
While I prefer to use libraries, sometimes it's not possible or the libraries don't fit my overall needs ... that and I like to dabble ... it's kind of a hobby.
Based on what I can understand from your code, you're trying to start out with a fast animation and then slow it down till you get to the last frame. In animation theory, this is commonly known as easement, more specifically, "slow/ease out".
The following borrows from a bunch of snippets I've been playing with (to devise a more reusable library) that will basically (randomly) display the images over a period of 4 seconds, with the animation slowing down and finally, presenting the "lucky" number
nb The gif animation is actually really slow, you'll need to run it to see the difference
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.*;
public class Reveal extends JPanel {
public static void main(String[] args) {
new Reveal();
}
public Reveal() {
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 IntAnimatable animatable;
private List<ImageIcon> icons = new ArrayList<>(25);
private JLabel label = new JLabel();
public TestPane() {
setLayout(new GridBagLayout());
IntRange range = new IntRange(0, 111);
animatable = new IntAnimatable(range, Duration.ofSeconds(4), Easement.SLOWOUT, new AnimatableListener<Integer>() {
#Override
public void animationChanged(Animatable<Integer> animator) {
int value = animator.getValue();
int index = value % 7;
ImageIcon icon = icons.get(index);
if (label.getIcon() != icon) {
label.setIcon(icon);
}
}
}, new AnimatableLifeCycleAdapter<Integer>() {
#Override
public void animationCompleted(Animatable<Integer> animator) {
BufferedImage img = makeImage(3);
writeTextOverImage("Lucky number", img);
ImageIcon luckNumber = new ImageIcon(img);
label.setIcon(luckNumber);
}
});
for (int index = 0; index < 7; index++) {
icons.add(new ImageIcon(makeImage(index)));
}
Collections.shuffle(icons);
add(label);
Animator.INSTANCE.add(animatable);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
}
protected void writeTextOverImage(String text, BufferedImage img) {
Graphics2D g2d = img.createGraphics();
Font font = g2d.getFont();
font = font.deriveFont(Font.BOLD, font.getSize2D() + 2);
g2d.setFont(font);
FontMetrics fm = g2d.getFontMetrics();
int width = img.getWidth();
int height = img.getWidth();
int x = (width - fm.stringWidth(text)) / 2;
int y = fm.getAscent();
g2d.setColor(Color.YELLOW);
g2d.drawString(text, x, y);
g2d.dispose();
}
protected BufferedImage makeImage(int h) {
BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = img.createGraphics();
FontMetrics fm = g2d.getFontMetrics();
String text = Integer.toString(h);
int x = (100 - fm.stringWidth(text)) / 2;
int y = ((100 - fm.getHeight()) / 2) + fm.getAscent();
g2d.setColor(Color.BLUE);
g2d.fillRect(0, 0, 100, 100);
g2d.setColor(Color.WHITE);
g2d.drawString(text, x, y);
g2d.dispose();
return img;
}
/**** Range ****/
/*
A lot of animation is done from one point to another, this just
provides a self contained concept of a range which can be used to
calculate the value based on the current progression over time
*/
public abstract class Range<T> {
private T from;
private T to;
public Range(T from, T to) {
this.from = from;
this.to = to;
}
public T getFrom() {
return from;
}
public T getTo() {
return to;
}
#Override
public String toString() {
return "From " + getFrom() + " to " + getTo();
}
public abstract T valueAt(double progress);
}
public class IntRange extends Range<Integer> {
public IntRange(Integer from, Integer to) {
super(from, to);
}
public Integer getDistance() {
return getTo() - getFrom();
}
#Override
public Integer valueAt(double progress) {
int distance = getDistance();
int value = (int) Math.round((double) distance * progress);
value += getFrom();
return value;
}
}
/**** Animatable ****/
/*
The core concept of something that is animatable. This basic wraps up the
logic for calculating the progression of the animation over a period of time
and then use that to calculate the value of the range and then the observers
are notified so they can do stuff
*/
public class IntAnimatable extends AbstractAnimatableRange<Integer> {
public IntAnimatable(IntRange animationRange, Duration duration, Easement easement, AnimatableListener<Integer> listener, AnimatableLifeCycleListener<Integer> lifeCycleListener) {
super(animationRange, duration, easement, listener, lifeCycleListener);
}
}
public interface AnimatableListener<T> {
public void animationChanged(Animatable<T> animator);
}
public interface AnimatableLifeCycleListener<T> {
public void animationStopped(Animatable<T> animator);
public void animationCompleted(Animatable<T> animator);
public void animationStarted(Animatable<T> animator);
public void animationPaused(Animatable<T> animator);
}
public interface Animatable<T> {
public T getValue();
public void tick();
public Duration getDuration();
public Easement getEasement();
// Wondering if these should be part of a secondary interface
// Provide a "self managed" unit of work
public void start();
public void stop();
public void pause();
}
public class AnimatableLifeCycleAdapter<T> implements AnimatableLifeCycleListener<T> {
#Override
public void animationStopped(Animatable<T> animator) {
}
#Override
public void animationCompleted(Animatable<T> animator) {
}
#Override
public void animationStarted(Animatable<T> animator) {
}
#Override
public void animationPaused(Animatable<T> animator) {
}
}
public abstract class AbstractAnimatable<T> implements Animatable<T> {
private LocalDateTime startTime;
private Duration duration = Duration.ofSeconds(5);
private AnimatableListener<T> animatableListener;
private AnimatableLifeCycleListener<T> lifeCycleListener;
private Easement easement;
private double rawOffset;
public AbstractAnimatable(Duration duration, AnimatableListener<T> listener) {
this.animatableListener = listener;
this.duration = duration;
}
public AbstractAnimatable(Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
this(duration, listener);
this.lifeCycleListener = lifeCycleListener;
}
public AbstractAnimatable(Duration duration, Easement easement, AnimatableListener<T> listener) {
this(duration, listener);
this.easement = easement;
}
public AbstractAnimatable(Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
this(duration, easement, listener);
this.lifeCycleListener = lifeCycleListener;
}
public void setEasement(Easement easement) {
this.easement = easement;
}
#Override
public Easement getEasement() {
return easement;
}
public Duration getDuration() {
return duration;
}
protected void setDuration(Duration duration) {
this.duration = duration;
}
public double getCurrentProgress(double rawProgress) {
Easement easement = getEasement();
double progress = Math.min(1.0, Math.max(0.0, getRawProgress()));
if (easement != null) {
progress = easement.interpolate(progress);
}
return Math.min(1.0, Math.max(0.0, progress));
}
public double getRawProgress() {
if (startTime == null) {
return 0.0;
}
Duration duration = getDuration();
Duration runningTime = Duration.between(startTime, LocalDateTime.now());
double progress = rawOffset + (runningTime.toMillis() / (double) duration.toMillis());
return Math.min(1.0, Math.max(0.0, progress));
}
#Override
public void tick() {
if (startTime == null) {
startTime = LocalDateTime.now();
fireAnimationStarted();
}
double rawProgress = getRawProgress();
double progress = getCurrentProgress(rawProgress);
if (rawProgress >= 1.0) {
progress = 1.0;
}
tick(progress);
fireAnimationChanged();
if (rawProgress >= 1.0) {
fireAnimationCompleted();
}
}
protected abstract void tick(double progress);
#Override
public void start() {
if (startTime != null) {
// Restart?
return;
}
Animator.INSTANCE.add(this);
}
#Override
public void stop() {
stopWithNotitifcation(true);
}
#Override
public void pause() {
rawOffset += getRawProgress();
stopWithNotitifcation(false);
double remainingProgress = 1.0 - rawOffset;
Duration remainingTime = getDuration().minusMillis((long) remainingProgress);
setDuration(remainingTime);
lifeCycleListener.animationStopped(this);
}
protected void fireAnimationChanged() {
if (animatableListener == null) {
return;
}
animatableListener.animationChanged(this);
}
protected void fireAnimationCompleted() {
stopWithNotitifcation(false);
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationCompleted(this);
}
protected void fireAnimationStarted() {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationStarted(this);
}
protected void fireAnimationPaused() {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationPaused(this);
}
protected void stopWithNotitifcation(boolean notify) {
Animator.INSTANCE.remove(this);
startTime = null;
if (notify) {
if (lifeCycleListener == null) {
return;
}
lifeCycleListener.animationStopped(this);
}
}
}
public abstract class AbstractAnimatableRange<T> extends AbstractAnimatable<T> {
private Range<T> range;
private T value;
public AbstractAnimatableRange(Range<T> range, Duration duration, AnimatableListener<T> listener) {
super(duration, listener);
this.range = range;
}
public AbstractAnimatableRange(Range<T> range, Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
super(duration, listener, lifeCycleListener);
this.range = range;
}
public AbstractAnimatableRange(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener) {
super(duration, easement, listener);
this.range = range;
}
public AbstractAnimatableRange(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
super(duration, easement, listener, lifeCycleListener);
this.range = range;
}
protected void tick(double progress) {
setValue(range.valueAt(progress));
}
protected void setValue(T value) {
this.value = value;
}
#Override
public T getValue() {
return value;
}
}
/*
Easement, complicated, but fun
*/
public enum Easement {
SLOWINSLOWOUT(1d, 0d, 0d, 1d), FASTINSLOWOUT(0d, 0d, 1d, 1d), SLOWINFASTOUT(0d, 1d, 0d, 0d), SLOWIN(1d, 0d, 1d, 1d), SLOWOUT(0d, 0d, 0d, 1d);
private final double[] points;
private final List<PointUnit> normalisedCurve;
private Easement(double x1, double y1, double x2, double y2) {
points = new double[]{x1, y1, x2, y2};
final List<Double> baseLengths = new ArrayList<>();
double prevX = 0;
double prevY = 0;
double cumulativeLength = 0;
for (double t = 0; t <= 1; t += 0.01) {
Point2D xy = getXY(t);
double length = cumulativeLength + Math.sqrt((xy.getX() - prevX) * (xy.getX() - prevX) + (xy.getY() - prevY) * (xy.getY() - prevY));
baseLengths.add(length);
cumulativeLength = length;
prevX = xy.getX();
prevY = xy.getY();
}
normalisedCurve = new ArrayList<>(baseLengths.size());
int index = 0;
for (double t = 0; t <= 1; t += 0.01) {
double length = baseLengths.get(index++);
double normalLength = length / cumulativeLength;
normalisedCurve.add(new PointUnit(t, normalLength));
}
}
public double interpolate(double fraction) {
int low = 1;
int high = normalisedCurve.size() - 1;
int mid = 0;
while (low <= high) {
mid = (low + high) / 2;
if (fraction > normalisedCurve.get(mid).getPoint()) {
low = mid + 1;
} else if (mid > 0 && fraction < normalisedCurve.get(mid - 1).getPoint()) {
high = mid - 1;
} else {
break;
}
}
/*
* The answer lies between the "mid" item and its predecessor.
*/
final PointUnit prevItem = normalisedCurve.get(mid - 1);
final double prevFraction = prevItem.getPoint();
final double prevT = prevItem.getDistance();
final PointUnit item = normalisedCurve.get(mid);
final double proportion = (fraction - prevFraction) / (item.getPoint() - prevFraction);
final double interpolatedT = prevT + (proportion * (item.getDistance() - prevT));
return getY(interpolatedT);
}
protected Point2D getXY(double t) {
final double invT = 1 - t;
final double b1 = 3 * t * invT * invT;
final double b2 = 3 * t * t * invT;
final double b3 = t * t * t;
final Point2D xy = new Point2D.Double((b1 * points[0]) + (b2 * points[2]) + b3, (b1 * points[1]) + (b2 * points[3]) + b3);
return xy;
}
protected double getY(double t) {
final double invT = 1 - t;
final double b1 = 3 * t * invT * invT;
final double b2 = 3 * t * t * invT;
final double b3 = t * t * t;
return (b1 * points[2]) + (b2 * points[3]) + b3;
}
protected class PointUnit {
private final double distance;
private final double point;
public PointUnit(double distance, double point) {
this.distance = distance;
this.point = point;
}
public double getDistance() {
return distance;
}
public double getPoint() {
return point;
}
}
}
/**** Core Animation Engine ****/
public enum Animator {
INSTANCE;
private Timer timer;
private List<Animatable> properies;
private Animator() {
properies = new ArrayList<>(5);
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
List<Animatable> copy = new ArrayList<>(properies);
Iterator<Animatable> it = copy.iterator();
while (it.hasNext()) {
Animatable ap = it.next();
ap.tick();
}
if (properies.isEmpty()) {
timer.stop();
}
}
});
}
public void add(Animatable ap) {
properies.add(ap);
timer.start();
}
protected void removeAll(List<Animatable> completed) {
properies.removeAll(completed);
}
public void remove(Animatable ap) {
properies.remove(ap);
if (properies.isEmpty()) {
timer.stop();
}
}
}
}
When my animation is in progress figures get stuck together. I think it's because if Figure get velocity x=5 y=5 i move them and then check if they hit anything and my figure can be already inside 2nd figure.
I want to check if they hit anything more often but im not sure how to put my methods in actionPerformed method.
Velocity of figures is not constant.
Do you have any ideas, examples or suggestions?
public class PaintFigures extends JPanel implements ActionListener {
static List<Figure> figuresList = new ArrayList<Figure>();
Timer t = new Timer(5, this);
public PaintFigures(List<Figure> figuresList) {
PaintFigures.figuresList = figuresList;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
t.start();
for (Figure figure : figuresList) {
figure.drawItself(g2d);
}
}
#Override
public void actionPerformed(ActionEvent e) {
FiguresUpdate.update(figuresList); // Check if they hit anything (other figure or frame)
FiguresUpdate.move(figuresList); // move them
repaint();
}
}
Runnable Example Here
Class main
import java.awt.BorderLayout;
import java.awt.Color;
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.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Test extends JPanel implements ActionListener {
static private List<Square> figuresList = new ArrayList<Square>();
Timer t = new Timer(5, this);
public static void main(String[] args) {
Square s1 = new Square(40);
Square s2 = new Square(60);
Square s3 = new Square(20);
figuresList.add(s1);
figuresList.add(s3);
figuresList.add(s2);
JFrame frame = new JFrame("Figures Animation");
frame.setSize(700, 400);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new Test();
panel.setBackground(Color.GRAY);
frame.getContentPane().add(BorderLayout.CENTER, panel);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
t.start();
for (Square figure : figuresList) {
figure.drawItself(g2d);
}
}
#Override
public void actionPerformed(ActionEvent e) {
Test.update(figuresList); // Check if they bounce
// FiguresUpdate.move(figuresList); // move them
repaint();
}
public static void update(List<Square> list) {
updateFlags(list);
for (int i = 0; i < list.size(); i++) {
list.get(i).setLocationX(
list.get(i).getLocationX() + (list.get(i).getVelocityX()));
list.get(i).setLocationY(
list.get(i).getLocationY() + (list.get(i).getVelocityY()));
if (list.get(i).getLocationX() < 0
|| list.get(i).getLocationX() > 680 - (list.get(i)
.getWidth())) {
WallXBounceDetected(list.get(i));
}
if (list.get(i).getLocationY() < 0
|| list.get(i).getLocationY() > 360 - (list.get(i)
.getHeight())) {
WallYBounceDetected(list.get(i));
}
for (int j = i + 1; j < list.size(); j++) {
if (list.get(i).getBounds().intersects(list.get(j).getBounds())
&& (!list.get(i).getDidHeBounce())
&& (!list.get(j).getDidHeBounce())) {
System.out.println(list.get(i).getClass().getSimpleName());
FigureBounceDetected(list.get(i), list.get(j));
}
}
}
}
public static void updateFlags(List<Square> list) {
for (int i = 0; i < list.size(); i++) {
list.get(i).setDidHeBounce(false);
}
}
public static void WallXBounceDetected(Square f) {
f.setVelocityX(-f.getVelocityX());
}
public static void WallYBounceDetected(Square f) {
f.setVelocityY(-f.getVelocityY());
}
public static void FigureBounceDetected(Square f1, Square f2) {
// Elastic Collision
// Figure 1
double newSpeedF1X = (f1.getVelocityX() * (f1.getMass() - f2.getMass()) + (2 * f2
.getMass() * f2.getVelocityX()))
/ (f1.getMass() + f2.getMass());
double newSpeedF1Y = (f1.getVelocityY() * (f1.getMass() - f2.getMass()) + (2 * f2
.getMass() * f2.getVelocityY()))
/ (f1.getMass() + f2.getMass());
// Figure 2
double newSpeedF2X = (f2.getVelocityX() * (f2.getMass() - f1.getMass()) + (2 * f1
.getMass() * f1.getVelocityX()))
/ (f1.getMass() + f2.getMass());
double newSpeedF2Y = (f2.getVelocityY() * (f2.getMass() - f1.getMass()) + (2 * f1
.getMass() * f1.getVelocityX()))
/ (f1.getMass() + f2.getMass());
f1.setLocationX(f1.getLocationX() + (newSpeedF1X));
f1.setLocationY(f1.getLocationY() + (newSpeedF1Y));
f2.setLocationX(f2.getLocationX() + (newSpeedF2X));
f2.setLocationY(f2.getLocationY() + (newSpeedF2Y));
// new velocity
f1.setVelocityX(newSpeedF1X);
f1.setVelocityY(newSpeedF1Y);
f2.setVelocityX(newSpeedF2X);
f2.setVelocityY(newSpeedF2Y);
// flag true
f1.setDidHeBounce(true);
f2.setDidHeBounce(true);
}
}
Class Square.
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.util.Random;
public class Square {
Rectangle2D.Double square;
private double locationX = 120;
private double locationY = 120;
private double velocityX =1;
private double velocityY =1;
private double width;
private double height = width;
private double mass = width;
private boolean didHeBounce=false;
Color color;
public Square(int width) {
this.width = width;
height = this.width;
mass = height;
Random r = new Random();
if(r.nextInt(2)>0){
velocityX=-1;
} else {
velocityX=1;
}
if(r.nextInt(2)>0){
velocityY=-1;
} else {
velocityY=1;
}
locationX =r.nextInt(540);
locationY= r.nextInt(220);
}
public void drawItself(Graphics g){
Graphics2D g2d = (Graphics2D) g;
square = new Rectangle2D.Double(locationX,locationY,height,width);
g2d.fill(square);
g.setColor(Color.BLUE);
}
public boolean getDidHeBounce() {
return didHeBounce;
}
public void setDidHeBounce(boolean didHeBounce){
this.didHeBounce = didHeBounce;
}
public double getLocationX() {
return locationX;
}
public void setLocationX(double locationX) {
this.locationX = locationX;
}
public double getLocationY() {
return locationY;
}
public void setLocationY(double locationY) {
this.locationY = locationY;
}
public double getVelocityX() {
return velocityX;
}
public void setVelocityX(double velocityX) {
this.velocityX = velocityX;
}
public double getVelocityY() {
return velocityY;
}
public void setVelocityY(double velocityY) {
this.velocityY = velocityY;
}
public double getMass() {
return mass;
}
public double getHeight() {
return height;
}
public double getWidth() {
return width;
}
public Rectangle2D getBounds() {
return square.getBounds2D();
}
}
Hello first of all you moved them here:
f1.setLocationX(f1.getLocationX() + (newSpeedF1X));
f1.setLocationY(f1.getLocationY() + (newSpeedF1Y));
f2.setLocationX(f2.getLocationX() + (newSpeedF2X));
f2.setLocationY(f2.getLocationY() + (newSpeedF2Y));
So it's not frequency problem.
Problem has to be in formula and it is.
You got:
//figure 1
newSpeedF1X = velocityXF1*(MassF1-MassF2)+(2*MassF2*VelocityXF2)/(MassF1+MassF2)
newSpeedF1Y = velocityYF1*(MassF1-MassF2)+(2*MassF2*VelocityXF2)/(MassF1+MassF2)
// Figure 2
newSpeedF2X = velocityXF2*(MassF2-MassF1)+(2*MassF1*VelocityXF1)/(MassF1+MassF2)
newSpeedF2Y = velocityYF2*(MassF2-MassF1)+(2*MassF1*VelocityXF1)/(MassF1+MassF2)
And in newSpeedF2Y should be VelocityYF1 not X
newSpeedF2Y = velocityYF2*(MassF2-MassF1)+(2*MassF1*VelocityYF1)/(MassF1+MassF2)
Additional remarks
Wall bounce detect:
I noticed your figures getting stuck in walls and you change their velocity to -velocity when they are out of bunds so they cant get out.
To avoid figures getting stuck in wall you should do something like this:
public void wallXBounceDetect(Figure f) {
f.setVelocityX(wallBounceDetect(f.getLocationX(), f.getWidth(), canvas.getWidth(), f.getVelocityX()));
}
public void wallYBounceDetect(Figure f) {
f.setVelocityY(wallBounceDetect(f.getLocationY(), f.getHeight(), canvas.getHeight(), f.getVelocityY()));
}
public double wallBounceDetect(double location, double size, double maxValue, double velocity) {
if ((location < 0 && velocity < 0) || (location + size > maxValue && velocity > 0)) {
return -velocity;
}
return velocity;
}
Where canvas is your class with method PaintComponent which extends JPanel.
It has to do with the laws you are applying. If the bouncing is not long enough they will probably stick forever. A simple rule: if the two figures collide and figure 1 is lower than figure 2 (f1.xf2.x) f1 is bounced a bit back otherwise (f1.x>f2.x) it's bounced a bit forward. It seems to work for me right now. You need to check the laws and what values they give (newSpeedF1X etc)
public static void FigureBounceDetected(Square f1, Square f2) {
// Elastic Collision
// Figure 1
double newSpeedF1X = (f1.getVelocityX() * (f1.getMass() - f2.getMass()) + (2 * f2
.getMass() * f2.getVelocityX()))
/ (f1.getMass() + f2.getMass());
double newSpeedF1Y = (f1.getVelocityY() * (f1.getMass() - f2.getMass()) + (2 * f2
.getMass() * f2.getVelocityY()))
/ (f1.getMass() + f2.getMass());
// Figure 2
double newSpeedF2X = (f2.getVelocityX() * (f2.getMass() - f1.getMass()) + (2 * f1
.getMass() * f1.getVelocityX()))
/ (f1.getMass() + f2.getMass());
double newSpeedF2Y = (f2.getVelocityY() * (f2.getMass() - f1.getMass()) + (2 * f1
.getMass() * f1.getVelocityX()))
/ (f1.getMass() + f2.getMass());
System.out.println("prev "+f1.getprevx()+" "+newSpeedF1X+" "+newSpeedF1Y+" "+newSpeedF2X+" "+newSpeedF2Y);
// f1.setLocationX(f1.getLocationX() + (newSpeedF1X));
// f1.setLocationY(f1.getLocationY() + (newSpeedF1Y));
// f2.setLocationX(f2.getLocationX() + (newSpeedF2X));
// f2.setLocationY(f2.getLocationY() + (newSpeedF2Y));
if(f1.getLocationX()<f2.getLocationX()) f1.setLocationX(Math.max(0, f1.getLocationX()-f1.getWidth()));
else f1.setLocationX(Math.min(700-f1.getWidth(), f1.getLocationX()+f1.getWidth()));
// new velocity
// f1.setVelocityX(newSpeedF1X);
// f1.setVelocityY(newSpeedF1Y);
// f2.setVelocityX(newSpeedF2X);
// f2.setVelocityY(newSpeedF2Y);
// flag true
f1.setDidHeBounce(true);
f2.setDidHeBounce(true);
}
}
Some other changes are minor
for (int j = i + 1; j < list.size(); j++) {
int ij=j%list.size();
if (list.get(i).getBounds().intersects(list.get(j).getBounds())
&& (!list.get(i).getDidHeBounce())
&& (!list.get(ij).getDidHeBounce())
) {
System.out.println(list.get(i).getClass().getSimpleName());
FigureBounceDetected(list.get(i), list.get(ij));
}
public void drawItself(Graphics2D g){
Color c=g.getColor();
Graphics2D g2d = (Graphics2D) g;
square = new Rectangle2D.Double(locationX,locationY,height,width);
g.setColor(Color.BLUE);
g.fill(square);
g.setColor(c);
}