-4

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

Ruthvik
  • 790
  • 5
  • 28
  • 6
    Please provide a [mre]. As an aside, I hope you realise that floating point operations are by definition not exact, so some error is to be expected. – Mark Rotteveel Jul 20 '22 at 14:15
  • 3
    The code you've shown never calls `Math.toRadians`. – Silvio Mayolo Jul 20 '22 at 14:16
  • 1
    @SilvioMayolo, that code is in another function, that code is just to show what instructions im using – Ruthvik Jul 20 '22 at 14:18
  • 1
    @MarkRotteveel, please check the code which I have added. – Ruthvik Jul 20 '22 at 14:23
  • The requirements for questions are described in the help center, if you don't want your question to get closed, then make sure that your question contains the required information from the start. – Mark Rotteveel Jul 20 '22 at 14:26
  • @MarkRotteveel, So does that mean that this problem will be lost to history and never be solved now??? – Ruthvik Jul 20 '22 at 14:28
  • @Ruthvik thats True. I have been asking my professors about this issue just yesterday (even he didnt have a clear cut explanation for this) and luckily I see a post with a similar question. Its so sad to see it getting closed without actually getting answers. – Learner Jul 20 '22 at 14:29
  • 4
    I have voted to reopen, and it will go to the reopen review queue. – Mark Rotteveel Jul 20 '22 at 14:29
  • Please note that you need a single line of code to call and print the result of `Math.toRadians()`. Then you will see if, as you claim, "Math.toRadians() is not accurate". – Olivier Jul 20 '22 at 14:33
  • 2
    Common case for alike artifacts is error accumulation due to using of integer values for calculation. Instead you can store internal position as float values, calculate new position in float, and round it only for graphic output. – MBo Jul 20 '22 at 18:04
  • 1
    There is zero chance that Math.toRadians() a base function in one of the most widespread math libraries in existence, is not accurate enough for normal use. It would have been caught in unit tests years ago. Your problem is elsewhere. However you don't make it easy to help you when you don't include the actual code you think is broken, and you blame it on the wrong problem in the title. – Gabe Sechan Jul 20 '22 at 21:21
  • In any case you don't need degree to rad conversion. Since 360° = 2 PI therefore 2PI rad / 6 = 72 ° no need degrees for cos and sin. Check this https://stackoverflow.com/a/72502727/7587451 – Giovanni Contreras Jul 20 '22 at 23:48
  • "Math.toRadians() is not accurate. there is no issue with my code" That's quite a bold claim. What leads you to think that `Math.toRadians()` specifically is at fault? There's a lot of code in what you posted, and the calls to `Math.toRadians()` are only a small part of that code. – Mark Dickinson Jul 21 '22 at 06:33
  • @MarkDickinson, There you go, I have made an edit to the post to show you that `Math.toRadians()` itself is the real fishy thing – Ruthvik Jul 21 '22 at 16:01
  • @Ruthvik: Those are tiny errors. The effect of those errors would amount to differences on the order of trillionths of a pixel. Those errors are not the cause of the effect you're seeing. The cause of the effect you're seeing is that already mentioned by @MBo. It really is _your_ code that's causing the issue, not `Math.toRadians`. – Mark Dickinson Jul 21 '22 at 17:36
  • @MarkDickinson, when i rounded those values i no more get that error at all, so i can surely conclude that rounding thing itself was the error – Ruthvik Jul 22 '22 at 00:40

1 Answers1

2

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

Ruthvik
  • 790
  • 5
  • 28