move dot between 2 points jframe [closed]

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...

enter image description here

😮 ... 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...

  1. 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
  2. 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)
  3. 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
  4. 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...

enter image description here

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