0

I currently have a working code that draws a fractal tree using recursion. However, when I try to draw it iteratively, it is not working.

import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;

 /*

 -Used built in Math trig methods to accomodate angling...looked this up online


 https://stackoverflow.com/questions/30032635/java-swing-draw-a-line-at-a-specific-angle


*/



public class Test extends JFrame {

    public Test() {

        setBounds(100, 100, 800, 600);  //sets the boundary for drawing

    }

    public void drawTree(Graphics g, int x1, int y1, double angle, int depth) {

        System.out.println("x");

        if (depth == 6){
            return; //base case here to prevent infinite recursion..
        }
        else {


            System.out.println("y1");

            //embedded portion '(Math.toRadians(angle) * depth * 10.0)'represents angle...

            int x2 = x1 + (int) (Math.cos(Math.toRadians(angle)) * depth * 10.0);  //vertical shift calculated using the Sine...PARSED to int because method drawLine() accepts only ints as params
            int y2 = y1 + (int) (Math.sin(Math.toRadians(angle)) * depth * 10.0);  //hor. shift calculated using the Cos..PARSED to int


          //  System.out.println("x2: " + x2);//will reflect the change in vertical shift
            //System.out.println("y2: " + y2);//will reflect change in hor. shift

            g.drawLine(x1, y1, x2, y2);//value x1 equals previous line...in other word, start at where previously left off
            // notice that the end point (x2 and y2) becomes starting point for each successive call


            drawTree(g, x2, y2, angle - 20, depth - 1); //DRAWS LEFT SIDE?

           // drawTree(g, x2, y2, angle + 20, depth - 1); //DRAWS RIGHT SIDE?

        }



        if (depth == 6){
            return; //base case here to prevent infinite recursion..
        }
        else {


            System.out.println("y2");


            int x2 = x1 + (int) (Math.cos(Math.toRadians(angle)) * depth * 10.0);  //vertical shift calculated using the Sine...PARSED to int because method drawLine() accepts only ints as params
            int y2 = y1 + (int) (Math.sin(Math.toRadians(angle)) * depth * 10.0);  //hor. shift calculated using the Cos..PARSED to int


           // System.out.println("x2: " + x2);//will reflect the change in vertical shift
            //System.out.println("y2: " + y2);//will reflect change in hor. shift

            g.drawLine(x1, y1, x2, y2);//value x1 equals previous line...in other word, start at where previously left off
            // notice that the end point (x2 and y2) becomes starting point for each successive call


           // drawTree(g, x2, y2, angle - 20, depth - 1); //DRAWS LEFT SIDE?

            drawTree(g, x2, y2, angle + 20, depth - 1); //DRAWS RIGHT SIDE?

        }

    }




    public void drawIteratively(Graphics g, int x1A, int y1A, int x1B, int y1B, double angleA, double angleB, int depthA, int depthB){

        while (depthA != 4) {


            int x2A = x1A + (int) (Math.cos(Math.toRadians(angleA)) * depthA * 10.0);
            int y2A = y1A + (int) (Math.sin(Math.toRadians(angleA)) * depthA * 10.0);

            g.drawLine(x1A, y1A, x2A, y2A);   //remember it must continue drawing from where it left off

            angleA = angleA - 20;
            depthA = depthA - 1;

            x1A = x2A;
            y1A = y2A;

        }

        while (depthA != 4) {


            int x2A = x1A + (int) (Math.cos(Math.toRadians(angleA)) * depthA * 10.0);
            int y2A = y1A + (int) (Math.sin(Math.toRadians(angleA)) * depthA * 10.0);

            g.drawLine(x1A, y1A, x2A, y2A);   //remember it must continue drawing from where it left off

            angleA = angleA - 20;
            depthA = depthA - 1;

            x1A = x2A;
            y1A = y2A;

        }

        /*

        while(depthB != 4){


            int x2B = x1B + (int) (Math.cos(Math.toRadians(angleB)) * depthB * 10.0);
            int y2B = y1B + (int) (Math.sin(Math.toRadians(angleB)) * depthB * 10.0);

            g.drawLine(x1B, y1B, x2B, y2B);

            angleB = angleB + 20;
            depthB = depthB - 1;

            x1B = x2B;
            y1B = y2B;

        }
        */

    }






    @Override
    public void paint(Graphics g) {
        g.setColor(Color.BLUE);


       //drawTree(g, 400, 400, -90, 9); //these values corresponding to original line aka trunk during initial method call

        drawIteratively(g, 400, 500, 400,500 ,-90 , -90, 9,9);




    }

    public static void main(String[] args) {

        new Test().setVisible(true);

    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
jritzeku
  • 1
  • 1
  • 4
  • 1
    You shouldn't extend from `JFrame` and override it's `paint` method - there's a lot that goes into a frame which can affect the painting process - besides, you're breaking the paint chain. Instead, start with a `JPanel` and override it's `paintComponent` method (and don't forget to call `super.paintComponent`) – MadProgrammer Nov 30 '17 at 03:10
  • 1
    What you want is a Swing `Timer`, each iteration of the Swing `Timer` will update some state which adds to the fractal. You'll need to either draw this to a `BufferedImage` or maintain some kind of structure which can be used by the `paintComponet` method to render the current result – MadProgrammer Nov 30 '17 at 03:11
  • This will change the way your code works, because instead of using a recursive method, you'll need to maintain the information you need for each pass so it can seed the method – MadProgrammer Nov 30 '17 at 03:12
  • @MadProgrammer pedantry, but "its `paint` method," "its `paintComponent`" -- sorry, but it bugs me ;) – David Conrad Nov 30 '17 at 03:13
  • I think you need to reset `depthA` after first loop. – 001 Nov 30 '17 at 03:13
  • @JohnnyMopp I tried resetting depth but that created another problem. – jritzeku Nov 30 '17 at 03:48

2 Answers2

2

Ignoring the fractals math: if you want to keep the recursive drawing, warp the long process (recursive calculation) with a SwingWorker, and let it update the GUI. Here is an example:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

public class RecursiveDraw extends JFrame {

    private int x1A, y1A, x2A, y2A;
    private final int W = 700, H = 500;
    private Random random = new Random();
    private Color randomColor = Color.BLUE;
    private JPanel panel;

    public RecursiveDraw() {

        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        panel = new MyPanel();
        add(panel, BorderLayout.CENTER);
        pack();
        setVisible(true);
        new Task().run();
    }

    public void recursiveDraw(int x1A, int y1A, int depth){

        if(depth > 15) { return;}

        this.x1A = x1A; this.y1A = y1A;

        x2A = random.nextInt(W);
        y2A = random.nextInt(H);
        randomColor = new Color(random.nextInt(0xFFFFFF));
        panel.repaint();

        try {
            Thread.sleep(1000); //delay
        } catch (InterruptedException ex) { ex.printStackTrace();}

        recursiveDraw(x2A, y2A, ++depth );
    }

    class MyPanel extends JPanel{

        public MyPanel() {
            setPreferredSize(new Dimension(W,H));
        }

        @Override
        public void paintComponent(Graphics g) {
            //super.paintComponent(g);  //requires storing all points calculated
                                        //so they can be redrawn. recommended
            g.setColor(randomColor);
            g.drawLine(x1A, y1A, x2A, y2A);
        }
    }

    class Task extends SwingWorker<Void,Void> {

        @Override
        public Void doInBackground() {
            recursiveDraw(W/2, H/2, 0);
            return null;
        }
    }

    public static void main(String[] args) {

        new RecursiveDraw();
    }
}

The basic structure of iterative drawing, using Timer, as proposed by @MadProgrammer could look like this :

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random; 
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class TimerIterativeDraw extends JFrame {

    private final static int W = 700, H = 500;
    private final static int DELAY= 1000;
    private final static int NUMBER_OF_DRAWS_LIMIT = 50;
    private int x2A = W/2, y2A = H/2, x1A, y1A, numberOfDraws;
    private Random random = new Random();
    private Color randomColor = Color.BLUE;
    private JPanel panel;
    private Timer timer;

    public TimerIterativeDraw() {

        setDefaultCloseOperation(DISPOSE_ON_CLOSE);
        panel = new MyPanel();
        add(panel, BorderLayout.CENTER);
        pack();
        setVisible(true);

        timer = new Timer(DELAY,new Task());
        timer.start();
    }

    public void upDateGui(){

         if(numberOfDraws++ >= NUMBER_OF_DRAWS_LIMIT){
             timer.stop();
         }
        x1A = x2A; y1A = y2A;
        x2A = random.nextInt(W);
        y2A = random.nextInt(H);
        randomColor = new Color(random.nextInt(0xFFFFFF));

        //for better implementation store all points in an array list
        //so they can be redrawn

        panel.repaint();
    }

    class MyPanel extends JPanel{

        public MyPanel() {
            setPreferredSize(new Dimension(W,H));
        }

        @Override
        public void paintComponent(Graphics g) {
            //super.paintComponent(g);  //requires storing all points calculated
                                        //so they can be redrawn. recommended
            g.setColor(randomColor);
            g.drawLine(x1A, y1A, x2A, y2A);
        }
    }

    class Task implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent arg0) {
            upDateGui();
        }
    }

    public static void main(String[] args) {

        new TimerIterativeDraw();
    }
}
c0der
  • 18,467
  • 6
  • 33
  • 65
  • Not calling `super.paintComponent` will generate paint artefacts from other components which might appear on the UI - better to use `BufferedImage` or maintain some structure which can re-generated – MadProgrammer Nov 30 '17 at 07:45
  • Yes, I am aware that `super.paintComponent(g);` is required, hence my comment `//super.paintComponent(g); requires storing all points calculated so they can be redrawn. recommended`. I did not implement it to keep the example as basic and as simple as I could. – c0der Nov 30 '17 at 10:13
  • 1
    Don't comment, show, the problem is the OP will ignore and will have only learnt to do the wrong thing - there are no short cuts – MadProgrammer Nov 30 '17 at 11:45
1

There's probably a few ways you can do this, but...

  • You need some kind of class which you can call which will calculate the next step and record it
  • You need to make sure that you're only updating the state from within the context of the Event Dispatching Thread. This is important, as you don't want to update the UI or anything the UI might rely on out side the EDT, otherwise you run the risk of race conditions and dirty paints

So, first, we need some way to create the branches in some kind of stepped manner. The idea is to only generate a new branch each time the class is told to update.

The class will contain it's only state and management, but will provide access to a List of points which it has created, maybe something like...

public class Generator  {

    private List<Point> points;
    private double angle;
    private double delta;
    private int depth = 9;

    private Timer timer;

    public Generator(Point startPoint, double startAngle, double delta) {
        points = new ArrayList<>(25);
        points.add(startPoint);
        angle = startAngle;
        this.delta = delta;
    }

    public List<Point> getPoints() {
        return new ArrayList<Point>(points);
    }

    public boolean tick() {
        Point next = updateTree(points.get(points.size() - 1), angle);
        angle += delta;
        depth--;
        if (next != null) {
            points.add(next);
        }
        return next != null;
    }

    public Point updateTree(Point p, double angle) {

        if (depth == 6) {
            return null;
        }

        System.out.println("depth = " + depth + "; angle = " + angle);

        //embedded portion '(Math.toRadians(angle) * depth * 10.0)'represents angle...
        int x2 = p.x + (int) (Math.cos(Math.toRadians(angle)) * depth * 10.0);  //vertical shift calculated using the Sine...PARSED to int because method drawLine() accepts only ints as params
        int y2 = p.y + (int) (Math.sin(Math.toRadians(angle)) * depth * 10.0);  //hor. shift calculated using the Cos..PARSED to int

        return new Point(x2, y2);
    }

}

Now, this class only generates a single branch, in order to make a tree, you will need two instances of this class, with different deltas

Next, we need someway to ask this generator to generate the next step on a regular bases. For me, this typically invokes using a Swing Timer.

The reason been:

  • It's simple. Seriously, it's really simple
  • It won't block the EDT, thus not freezing the UI
  • It updates within the context of the EDT, making it safe to update the state of the UI from within.

Putting these two things together into a simple JPanel which controls the Timer and paints the points...

public class TestPane extends JPanel {

    private Generator left;
    private Generator right;

    public TestPane() {
        Point startPoint = new Point(200, 400);
        left = new Generator(startPoint, -90, -20);
        right = new Generator(startPoint, -90, 20);

        Timer timer = new Timer(1000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                boolean shouldContinue = left.tick() && right.tick();
                if (!shouldContinue) {
                    ((Timer)(e.getSource())).stop();
                }
                repaint();
            }
        });
        timer.start();
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(400, 400);
    }

    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setColor(Color.RED);
        render(g2d, left.getPoints());
        g2d.setColor(Color.BLUE);
        render(g2d, right.getPoints());
        g2d.dispose();
    }

    protected void render(Graphics2D g2d, List<Point> points) {
        Point start = points.remove(0);
        while (points.size() > 0) {
            Point end = points.remove(0);
            g2d.draw(new Line2D.Double(start, end));
            start = end;
        }
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366