简体   繁体   中英

Why Java Math.toRadians() is not accurate and how to solve it?

I'm creating own Turtle app similar to python in Java. The angles taken in the instructions is in degrees. While drawing it, I have to use sin and cos which requires the angle to be in radians. So, I used Math.toRadians(theta) .

My instructions are like:

moveTo(250, 250);
for (int i = 0; i < 60; i++) {
    forward(100);
    right(60);
}

I looped it for 60 times to check for errors. Here, I'm just making a hexagon and drawing over it again and again for 10 times, to check if the angles are working correctly. (If I find only one hexagon it means that the hexagon angles are working correctly, else there is some error)

Here is the image I got:
在此处输入图像描述

As you can see in the image, there is surely something fishy going in Math.toRadians() because after 6 rounds, it's not coming back to the same position.

Is there any way to solve this?

Image for one loop, here you can clearly see that its not making exactly 360 degrees

在此处输入图像描述

For those who want to see my code. (the question here is about why Math.toRadians() is not accurate. there is no issue with my code)

Here it is.

package com.example.jturtle;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.stream.IntStream;

public class JTurtle extends Application {

    private final ArrayList<String> instructions;
    private final ArrayList<String> previousInstructions;
    GraphicsContext gc;
    int i = 0;
    private boolean penDown;
    private int penX, penY;
    private int penSize;
    private int speed;
    private double theta;
    private Color penColor;
    private Color backgroundColor;

    public JTurtle() {
        penX = penY = 0;
        speed = 10;
        penSize = 1;
        penDown = true;
        theta = 0;
        penColor = Color.BLACK;
        backgroundColor = Color.WHITE;
        instructions = new ArrayList<>();
        previousInstructions = new ArrayList<>();
    }

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

    @Override
    public void start(Stage stage) {
        stage.setTitle("JTurtle");
        stage.setScene(new Scene(getPane()));
        stage.setResizable(false);
        stage.show();
    }

    private Parent getPane() {
        var canvas = new Canvas(500, 500);
        canvas.setLayoutX(0);
        canvas.setLayoutY(0);
        gc = canvas.getGraphicsContext2D();

        penSize(5);
        moveTo(250, 250);
        for (int i = 0; i < 6; i++) {
            forward(100);
            right(60);
        }

        new AnimationTimer() {
            @Override
            public void handle(long now) {
                update();
            }
        }.start();

        var pane = new AnchorPane(canvas);
        pane.setPrefSize(500, 500);
        return pane;
    }

    private void update() {
        try {
            boolean flag = IntStream.range(0, instructions.size()).anyMatch(i -> !instructions.get(i).equals(previousInstructions.get(i)));
            if (!flag) return;
        } catch (Exception ignored) {
        }

        for (String instruction : instructions) {
            var ins = instruction.split(" ");
            switch (ins[0]) {
                case "FWD" -> {
                    if (penDown)
                        gc.strokeLine(penX,
                                penY,
                                penX += Integer.parseInt(ins[1]) * Math.cos(Math.toRadians(theta)),
                                penY -= Integer.parseInt(ins[1]) * Math.sin(Math.toRadians(theta)));
                    else {
                        penX += Integer.parseInt(ins[1]) * Math.cos(Math.toRadians(theta));
                        penY -= Integer.parseInt(ins[1]) * Math.sin(Math.toRadians(theta));
                    }
                }
                case "BWD" -> {
                    if (penDown)
                        gc.strokeLine(penX,
                                penY,
                                penX += Integer.parseInt(ins[1]) * Math.cos(Math.toRadians(theta)),
                                penY += Integer.parseInt(ins[1]) * Math.sin(Math.toRadians(theta)));
                    else {
                        penX += Integer.parseInt(ins[1]) * Math.cos(Math.toRadians(theta));
                        penY += Integer.parseInt(ins[1]) * Math.sin(Math.toRadians(theta));
                    }
                }
                case "RGT" -> theta += Integer.parseInt(ins[1]);
                case "LFT" -> theta -= Integer.parseInt(ins[1]);
                case "PUP" -> penDown = false;
                case "PDN" -> penDown = true;
                case "PSZ" -> penSize = Integer.parseInt(ins[1]);
                case "PC" -> penColor = Color.web(ins[1]);
                case "BC" -> backgroundColor = Color.web(ins[1]);
                case "SPD" -> speed = Integer.parseInt(ins[1]);
                case "CLR" -> {
                    gc.setFill(backgroundColor);
                    gc.fillRect(0, 0, 500, 500);
                }
                case "MOV" -> {
                    penX = Integer.parseInt(ins[1]);
                    penY = Integer.parseInt(ins[2]);
                }
            }
        }
        previousInstructions.clear();
        previousInstructions.addAll(instructions);
    }

    public void forward(int distance) {
        instructions.add("FWD " + distance);
    }

    public void smoothForward(int distance) {
        instructions.add("SFD " + distance);
    }

    public void backward(int distance) {
        instructions.add("BWD " + distance);
    }

    public void smoothBackward(int distance) {
        instructions.add("SBW " + distance);
    }

    public void left(int angle) {
        instructions.add("LFT " + angle);
    }

    public void right(int angle) {
        instructions.add("RGT " + angle);
    }

    public void moveTo(int x, int y) {
        instructions.add("MOV " + x + " " + y);
    }

    public void penUp() {
        instructions.add("PUP");
    }

    public void penDown() {
        instructions.add("PDN");
    }

    public void penSize(int size) {
        instructions.add("PSZ " + size);
    }

    public void speed(int s) {
        instructions.add("SPD " + s);
    }

    public void penColor(Color c) {
        instructions.add("PC " + c.getRed() + " " + c.getGreen() + " " + c.getBlue());
    }

    public void backgroundColor(Color c) {
        instructions.add("BC " + c.getRed() + " " + c.getGreen() + " " + c.getBlue());
    }

    public void clear() {
        instructions.add("CLR");
    }
}

the question here is about why Math.toRadians() is not accurate. there is no issue with my code.

Some people here think this is a bold statement to be said. So I made some test

class Scratch {
    public static void main(String[] args) {
        for (int i = 0; i < 360; i++) {
            System.out.println(Math.toDegrees(Math.toRadians(i)));
        }
    }
}

and the output (a random segment of the printed thing)

245.00000000000003
246.00000000000003
247.0
248.0
248.99999999999997
250.00000000000003
251.0
252.0
253.0
254.00000000000003
255.00000000000003

So this clearly proves that Math.toRadians isnt correctly preserving the actual angle whose slight differences are actually ruining the whole drawing...

Well, I just solved it using Math.round()
like,

Math.round(Integer.parseInt(ins[1]) * Math.cos(Math.toRadians(theta)))

This may be the problem. As the coordinates get updated, continuous processing of previous coordinates can introduce floating point errors. Even if the math is sound. It's like making a copy of a copy. Here is an example. The triangle is rotated on its base. It's coordinates get updated over and over again and it starts to shrink due to the limitations of floating point math.

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class JTurtle extends JPanel {
    
    JFrame f = new JFrame();
    int length = 150;
    int size = 500;
    int ctrX = size / 2;
    int ctrY = size / 2;
    
    double[] tri = { -length / 2, 0, length / 2, 0, 0,
            -length * Math.sin(Math.PI / 3.), -length / 2, 0 };
    
    public static void main(String[] args) {
        
        SwingUtilities.invokeLater(() -> new JTurtle().start());
    }
    
    public void start() {
        f.add(this);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.pack();
        setBackground(Color.white);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
        Timer t = new Timer(0, (ae) -> rotate());
        t.setDelay(15);
        t.start();
    }
    
    public void rotate() {
        double a = Math.PI / 120;
        for (int i = 0; i < tri.length; i += 2) {;
            tri[i] = (tri[i]) * Math.cos(a)
                    - (tri[i + 1]) * Math.sin(a);
            tri[i + 1] = (tri[i]) * Math.sin(a)
                    + (tri[i + 1]) * Math.cos(a);
            repaint();
        }
    }
    
    public Dimension getPreferredSize() {
        return new Dimension(size, size);
    }
    
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setStroke(new BasicStroke(2));
        for (int i = 0; i < tri.length - 3; i += 2) {
            g2d.drawLine(ctrX + (int) tri[i], ctrY+(int) tri[i + 1],
                    ctrX+(int) (tri[i + 2]), ctrY+(int) tri[i + 3]);
        }
        g2d.setColor(Color.red);
        g2d.fillOval(ctrX - 5, ctrY - 5, 10, 10);
    }
    
}

If this is the situation, for each move you either need to update the original coordinates to the new location, or translate the graphics context to the new location and just move the original figure relative to its new position. As I am not versed in nor setup for JavaFX I could not run your code.

You must account for the size of the pen when you update penX and penY by adding or subtracting penSize/2 + 1 .

This will only work for odd pen sizes!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM