Java AWT 图形 - 以亚像素精度插值形状

Java AWT Graphics - Interpolate Shapes with subpixel accuracy

我正在做一个项目,我需要一个球在椭圆(例如圆周)上移动的动画。目前,我通过重写 paintComponent() 方法在 JPanel 上全部绘制,移动效果来自以固定速率重绘它,并改变“球”的位置.

一切都很好,除了球似乎以“梯子”方式移动,而且不流畅。既然是图之间的问题,我想乱搞RenderingHints是没有出路的。

一个问题的SSCCE(从测试来看,可能没有那么明显,但是当你有超过100个球在移动时,它看起来很奇怪):

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);
    }
}

所以总而言之,我认为也许我可以将位置插入到子像素级别,并以某种方式在“像素之间”绘制球,类似于抗锯齿效果的工作方式...可能吗?如果没有,是否有任何解决方法?

您的问题与将 double 值截断为 int

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

相反,您应该使用 double 值。在这种情况下,您可以简单地 translate Graphics2D API,例如...

// 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);

可运行示例

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();
        });
    }
}