How to make line animation smoother?
Asked Answered
W

2

2

I am making a simple animation in Java and I am trying to make it as smooth as possible.

I use only *.Double inner classes of each Shape object and I set antialiasing in the Graphics2D objects on. It all works as long as I use only the fill() method but if I also use draw() method to draw lines around the same Shape the animation of these lines is choppy - pixel by pixel.

Each of my rectangles on the canvas has this method to paint itself. It is moved every 20ms and the whole canvas is repainted using Timer and TimerListener.

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class AnimationTest {
    public static void main(String[] args) {
        JFrame frm = new JFrame("Test");
        frm.setBounds(200, 200, 400, 400);
        frm.setResizable(false);
        frm.setLocationRelativeTo(null);

        AnimationCanvas a = new AnimationCanvas();
        frm.add(a);

        frm.setVisible(true);

        a.startAnimation();
    }
}

class AnimationCanvas extends JPanel {

    SimpleSquare[] squares = new SimpleSquare[2];

    AnimationCanvas() {

        squares[0] = new SimpleSquare(50, 80, true);
        squares[1] = new SimpleSquare(160, 80, false);

    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        for (SimpleSquare c : squares) {
            c.paintSquare(g);
        }
    }

    Timer t;
    public void startAnimation() {
        t = new Timer(30, new Animator());
        t.start();
    }

    private class Animator implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            squares[0].y += 0.10;
            squares[1].y += 0.10;
            repaint();
        }
    }
}


class SimpleSquare {
    double x;
    double y;
    Color color = Color.black;
    boolean fill;

    SimpleSquare(double x, double y, boolean fill) {
        this.x = x;
        this.y = y;
        this.fill = fill;
    }

    void paintSquare(Graphics g) {
        ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        Shape s = new Rectangle.Double(x, y, 100, 100);

        g.setColor(color);
        ((Graphics2D) g).setStroke(new BasicStroke(2));

        if (fill) {
            ((Graphics2D) g).fill(s);
        } else {
            ((Graphics2D) g).draw(s);
        }
    }
}

Is there any way to fix this problem? I looked around for quite a while.

Windfall answered 24/11, 2012 at 10:57 Comment(1)
have you enabled double buffering?Cilla
C
5

I put together this little test and got no significnt issues, I was basically capable of maintaining 50fps even with 1000 rectangles all moving at random speeds in random directions.

enter image description here

public class SimpleAnimationEngine {

    public static void main(String[] args) {
        new SimpleAnimationEngine();
    }

    public SimpleAnimationEngine() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException ex) {
                } catch (InstantiationException ex) {
                } catch (IllegalAccessException ex) {
                } catch (UnsupportedLookAndFeelException ex) {
                }

                AnimationPane pane = new AnimationPane();

                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(pane);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);

                pane.init();
                pane.start();
            }

        });
    }

    public static class AnimationPane extends JPanel implements AnimationCanvas {

        private AnimationModel model;

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }

        public AnimationModel getModel() {
            return model;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            for (Animatable animatable : getModel().getAnimatables()) {
                animatable.paint(g2d);
            }
            g2d.dispose();
        }

        @Override
        public synchronized void updateState() {

            Runnable update = new Runnable() {
                @Override
                public void run() {
                    AnimationModel model = getModel();
                    for (Animatable animatable : model.getAnimatables()) {
                        animatable.copy();
                    }
                    repaint();
                }

            };

            if (EventQueue.isDispatchThread()) {
                update.run();
            } else {
                try {
                    EventQueue.invokeAndWait(update);
                } catch (InterruptedException | InvocationTargetException ex) {
                    ex.printStackTrace();
                }
            }
        }

        public void init() {
            model = new DefaultAnimationModel();
            for (int index = 0; index < 1000; index++) {
                model.add(new AnimatableRectangle(this));
            }
            updateState();
        }

        public void start() {
            AnimationEngine engine = new AnimationEngine(this, getModel());
            engine.start();
        }

    }

    public static interface Animatable {

        public void copy();

        public void update(AnimationCanvas canvas, float progress);

        public void paint(Graphics2D g2d);

    }

    public static class AnimationEngine extends Thread {

        private AnimationModel model;
        private AnimationCanvas canvas;

        public AnimationEngine(AnimationCanvas canvas, AnimationModel model) {
            setDaemon(true);
            setName("AnimationThread");
            this.model = model;
            this.canvas = canvas;
        }

        public AnimationCanvas getCanvas() {
            return canvas;
        }

        public AnimationModel getModel() {
            return model;
        }

        @Override
        public void run() {
            float progress = 0;
            long cylceStartTime = System.currentTimeMillis();
            long cylceEndTime = cylceStartTime + 1000;
            int updateCount = 0;
            while (true) {
                long frameStartTime = System.currentTimeMillis();
                getModel().update(getCanvas(), progress);
                getCanvas().updateState();
                long frameEndTime = System.currentTimeMillis();
                long delay = 20 - (frameEndTime - frameStartTime);
                if (delay > 0) {
                    try {
                        sleep(delay);
                    } catch (InterruptedException ex) {
                    }
                }
                long now = System.currentTimeMillis();
                long runtime = now - cylceStartTime;
                progress = (float)runtime / (float)(1000);
                updateCount++;
                if (progress > 1.0) {
                    progress = 0f;
                    cylceStartTime = System.currentTimeMillis();
                    cylceEndTime = cylceStartTime + 1000;
                    System.out.println(updateCount + " updates in this cycle");
                    updateCount = 0;
                }
            }
        }

    }

    public interface AnimationCanvas {

        public void updateState();

        public Rectangle getBounds();

    }

    public static interface AnimationModel {

        public void update(AnimationCanvas canvas, float progress);

        public void add(Animatable animatable);

        public void remove(Animatable animatable);

        public Animatable[] getAnimatables();

    }

    public static class AnimatableRectangle implements Animatable {

        private Rectangle bounds;
        private int dx, dy;
        private Rectangle copyBounds;
        private Color foreground;
        private Color backColor;

        public AnimatableRectangle(AnimationCanvas canvas) {
            bounds = new Rectangle(10, 10);
            Rectangle canvasBounds = canvas.getBounds();
            bounds.x = canvasBounds.x + ((canvasBounds.width - bounds.width) / 2);
            bounds.y = canvasBounds.y + ((canvasBounds.height - bounds.height) / 2);

            dx = (getRandomNumber(10) + 1) - 5;
            dy = (getRandomNumber(10) + 1) - 5;

            dx = dx == 0 ? 1 : dx;
            dy = dy == 0 ? 1 : dy;

            foreground = getRandomColor();
            backColor = getRandomColor();

        }

        protected int getRandomNumber(int range) {
            return (int) Math.round(Math.random() * range);
        }

        protected Color getRandomColor() {
            return new Color(getRandomNumber(255), getRandomNumber(255), getRandomNumber(255));
        }

        @Override
        public void copy() {
            copyBounds = new Rectangle(bounds);
        }

        @Override
        public void update(AnimationCanvas canvas, float progress) {
            bounds.x += dx;
            bounds.y += dy;
            Rectangle canvasBounds = canvas.getBounds();
            if (bounds.x + bounds.width > canvasBounds.x + canvasBounds.width) {
                bounds.x = canvasBounds.x + canvasBounds.width - bounds.width;
                dx *= -1;
            }
            if (bounds.y + bounds.height > canvasBounds.y + canvasBounds.height) {
                bounds.y = canvasBounds.y + canvasBounds.height - bounds.height;
                dy *= -1;
            }
            if (bounds.x < canvasBounds.x) {
                bounds.x = canvasBounds.x;
                dx *= -1;
            }
            if (bounds.y < canvasBounds.y) {
                bounds.y = canvasBounds.y;
                dy *= -1;
            }
        }

        @Override
        public void paint(Graphics2D g2d) {
            g2d.setColor(backColor);
            g2d.fill(copyBounds);
            g2d.setColor(foreground);
            g2d.draw(copyBounds);
        }

    }

    public static class DefaultAnimationModel implements AnimationModel {

        private List<Animatable> animatables;

        public DefaultAnimationModel() {
            animatables = new ArrayList<>(25);
        }

        @Override
        public synchronized void update(AnimationCanvas canvas, float progress) {
            for (Animatable animatable : animatables) {
                animatable.update(canvas, progress);
            }
        }

        @Override
        public synchronized void add(Animatable animatable) {
            animatables.add(animatable);
        }

        @Override
        public synchronized void remove(Animatable animatable) {
            animatables.remove(animatable);
        }

        @Override
        public synchronized Animatable[] getAnimatables() {
            return animatables.toArray(new Animatable[animatables.size()]);
        }

    }

}

UPDATE

The biggest problem you're going to face deals with the fact that the screen only works in whole numbers...

private class Animator implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        squares[0].y += 1;
        squares[1].y += 1;
        repaint();
    }
}

I believe that both squares are actually "jittering", but because the drawen square has such an obvious lack of body, it stands out more. I run this test at roughly 24fps without any issue.

Cenogenesis answered 25/11, 2012 at 3:3 Comment(3)
Well this is a rather complex code and mine is much much simpler and not very correct. I just use a simple Timer and repaint() the canvas every 20ms. But try to make 2 simple squares, one filled and one only drawn and then move them much slower then in your code. Move them with the speed of about 2px / sec. That way you will see that AA makes the filled square appear blurry bot not choppy but the drawn square will be sharp but choppy.Windfall
I'll try a replicate your problem, but this is why a working example is so importantCenogenesis
I edited original question and provided it with a working code. Feel free to comment on that. You can clearly see the square on the left moving smoothly and the one on the right moving choppy.Windfall
L
0

You have:

private class Animator implements ActionListener {
       @Override
       public void actionPerformed(ActionEvent e) {
           squares[0].y += 0.10;
           squares[1].y += 0.10;
           repaint();
       }
   }

And

public void startAnimation() {
    t = new Timer(30, new Animator());
    t.start();
}

But you could achieve the same by changing to

private class Animator implements ActionListener {
       @Override
       public void actionPerformed(ActionEvent e) {
           squares[0].y += 1;
           squares[1].y += 1;
           repaint();
       }
   }

And

public void startAnimation() {
    t = new Timer(300, new Animator());
    t.start();
}

Which does the same, but makes 10 times less loops.

I did not test the code but it is worth a try.

The proper solution is provided by MadProgrammer where he measures the delay and offsets by it.

Lavonda answered 26/11, 2012 at 2:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.