Search code examples
javamathradians

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:
enter image description here

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

enter image description here

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


Solution

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

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

    As mentioned in the question the radians is not exact and has tiny decimal error. which when rounded makes it to the nearest angle