Home > Enterprise >  Java AWT Graphics - Interpolate Shapes with subpixel accuracy
Java AWT Graphics - Interpolate Shapes with subpixel accuracy

Time:03-02

I'm working on a project where I need an animation of balls moving on an ellipse (e.g. a circumference). At the moment, I'm drawing it all on a JPanel, by overriding the paintComponent() method, and the moving effect comes from repainting it at a fixed rate, and changing the position of the "balls".

It all works good enough, except that the balls seem to move in a "laddery" way, and not smoothly. Since it's a problem between the drawings, I suppose messing with RenderingHints will lead nowhere.

An SSCCE of the issue (From the test, it might not seem that noticeable, but when you have more than 100 balls moving, it looks really weird):

import javax.swing.*;
import java.awt.*;

public class SSCCE {

    private static long dt;
    private static JPanel animationPanel = createAnimationPanel();

    private static JPanel createAnimationPanel() {
        return new JPanel() {
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);

                Graphics2D g2d = (Graphics2D) g;
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                
                int w = this.getWidth();
                int h = this.getHeight();
                Point center = new Point(w / 2, h / 2);

                // Drawing circumference.
                int radius = 80;
                int x = center.x - radius;
                int y = center.y - radius;
                g2d.drawOval(x, y, radius * 2, radius * 2);

                // Drawing ball.
                g2d.setColor(Color.RED);
                int ballWidth = 20;
                double rad = Math.toRadians((dt / ((radius * 2) / 8.0)));
                int xPos = (int) (center.x - (Math.cos(rad) * radius) - (ballWidth / 2));
                int yPos = (int) (center.y - (Math.sin(rad) * radius) - (ballWidth / 2));
                g2d.fillOval(xPos, yPos, ballWidth, ballWidth);
            }
        };
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Draw Test");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().setLayout(new BorderLayout());
            frame.getContentPane().add(animationPanel, BorderLayout.CENTER);
            frame.setSize(400, 400);
            frame.setLocationRelativeTo(null);
            frame.setEnabled(true);
            frame.setVisible(true);
            frame.requestFocus();
        });

        Thread updateThread = new Thread(() -> {
            long lastTime = System.currentTimeMillis();
            while (true) { // I don't actually use "while (true)", but it doesn't matter since it's a test.
                dt = System.currentTimeMillis() - lastTime;
                animationPanel.repaint();
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Update Thread");

        SwingUtilities.invokeLater(updateThread::start);
    }
}

So in conclusion, I thought that maybe I could interpolate the positions into subpixel level, and in some way draw the ball "between the pixels", in a way similar to how an Anti-alias effect works... Is it possible? If not, is there any workaround?

CodePudding user response:

You problem is related to the truncating of the double value to int

int xPos = (int) (center.x - (Math.cos(rad) * radius) - (ballWidth / 2));
int yPos = (int) (center.y - (Math.sin(rad) * radius) - (ballWidth / 2));

Instead, you should be making use the double values. In this case, you can simply translate the Graphics2D API, for example...

// Drawing ball.
g2d.setColor(Color.RED);
int ballWidth = 20;
double rad = Math.toRadians((dt / ((radius * 2) / 8.0)));
double xPos = (center.x - (Math.cos(rad) * radius) - (ballWidth / 2));
double yPos = (center.y - (Math.sin(rad) * radius) - (ballWidth / 2));

if (theActualDot == null) {
   theActualDot = new Ellipse2D.Double(0, 0, ballWidth, ballWidth);
}

g2d.translate(xPos, yPos);
g2d.fill(theActualDot);

Runnable example

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Main {

    private static long dt;
    private static JPanel animationPanel = createAnimationPanel();

    private static JPanel createAnimationPanel() {
        return new JPanel() {

            private Shape theActualDot;

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

                Graphics2D g2d = (Graphics2D) g.create();
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

                int w = this.getWidth();
                int h = this.getHeight();
                Point center = new Point(w / 2, h / 2);

                // Drawing circumference.
                int radius = 80;
                int x = center.x - radius;
                int y = center.y - radius;
                g2d.drawOval(x, y, radius * 2, radius * 2);

                // Drawing ball.
                g2d.setColor(Color.RED);
                int ballWidth = 20;
                double rad = Math.toRadians((dt / ((radius * 2) / 8.0)));
                double xPos = (center.x - (Math.cos(rad) * radius) - (ballWidth / 2));
                double yPos = (center.y - (Math.sin(rad) * radius) - (ballWidth / 2));

                if (theActualDot == null) {
                    theActualDot = new Ellipse2D.Double(0, 0, ballWidth, ballWidth);
                }

                g2d.translate(xPos, yPos);
                g2d.fill(theActualDot);
                g2d.dispose();
            }
        };
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame frame = new JFrame("Draw Test");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().setLayout(new BorderLayout());
            frame.getContentPane().add(animationPanel, BorderLayout.CENTER);
            frame.setSize(400, 400);
            frame.setLocationRelativeTo(null);
            frame.setEnabled(true);
            frame.setVisible(true);
            frame.requestFocus();

            Timer timer = new Timer(5, new ActionListener() {
                long lastTime = System.currentTimeMillis();

                @Override
                public void actionPerformed(ActionEvent e) {
                    dt = System.currentTimeMillis() - lastTime;
                    animationPanel.repaint();
                }
            });
            timer.start();
        });
    }
}
  • Related