Как рисовать фрактал итеративно?

В настоящее время у меня есть рабочий код, который рисует фрактальное дерево с использованием рекурсии. Однако, когда я пытаюсь нарисовать его итеративно, он не работает.

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

    }
}

person jritzeku    schedule 30.11.2017    source источник
comment
Вы не должны расширяться от JFrame и переопределять его метод paint — в кадр входит многое, что может повлиять на процесс рисования — кроме того, вы нарушаете цепочку рисования. Вместо этого начните с JPanel и переопределите его метод paintComponent (и не забудьте вызвать super.paintComponent)   -  person MadProgrammer    schedule 30.11.2017
comment
То, что вам нужно, это Swing Timer, каждая итерация Swing Timer будет обновлять некоторое состояние, которое добавляется к фракталу. Вам нужно будет либо нарисовать это в BufferedImage, либо сохранить какую-то структуру, которая может использоваться методом paintComponet для отображения текущего результата.   -  person MadProgrammer    schedule 30.11.2017
comment
Это изменит способ работы вашего кода, потому что вместо использования рекурсивного метода вам нужно будет поддерживать информацию, необходимую для каждого прохода, чтобы он мог заполнить метод.   -  person MadProgrammer    schedule 30.11.2017
comment
Педантизм @MadProgrammer, но его метод paint, его paintComponent - извините, но меня это раздражает;)   -  person David Conrad    schedule 30.11.2017
comment
Я думаю, вам нужно сбросить depthA после первого цикла.   -  person Johnny Mopp    schedule 30.11.2017
comment
@JohnnyMopp Я попытался сбросить глубину, но это создало еще одну проблему.   -  person jritzeku    schedule 30.11.2017


Ответы (2)


Игнорирование фрактальной математики: если вы хотите сохранить рекурсивный рисунок, деформируйте длинный процесс (рекурсивный расчет) с помощью SwingWorker и позволить ему обновить графический интерфейс. Вот пример:

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

Базовая структура итеративного рисования с использованием Timer, предложенная @MadProgrammer, может выглядеть так:

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();
    }
}
person c0der    schedule 30.11.2017
comment
Отсутствие вызова super.paintComponent приведет к созданию артефактов рисования из других компонентов, которые могут появиться в пользовательском интерфейсе — лучше использовать BufferedImage или поддерживать некоторую структуру, которая может быть сгенерирована повторно. - person MadProgrammer; 30.11.2017
comment
Да, я знаю, что требуется super.paintComponent(g);, поэтому мой комментарий //super.paintComponent(g); requires storing all points calculated so they can be redrawn. recommended. Я не реализовал его, чтобы сделать пример максимально простым и простым. - person c0der; 30.11.2017
comment
Не комментируйте, покажите, проблема в том, что ОП проигнорирует и только научится делать неправильные вещи - коротких путей нет - person MadProgrammer; 30.11.2017

Вероятно, есть несколько способов сделать это, но...

  • Вам нужен какой-то класс, который вы можете вызвать, который рассчитает следующий шаг и запишет его.
  • Вам нужно убедиться, что вы обновляете состояние только из контекста потока диспетчеризации событий. Это важно, так как вы не хотите обновлять пользовательский интерфейс или что-либо, на что пользовательский интерфейс может полагаться за пределами EDT, иначе вы рискуете получить условия гонки и грязные краски.

Итак, во-первых, нам нужно каким-то образом создать ветки в какой-то пошаговой манере. Идея состоит в том, чтобы генерировать новую ветку только каждый раз, когда классу предлагается обновить.

Класс будет содержать только его состояние и управление, но будет предоставлять доступ к List точкам, которые он создал, может быть, что-то вроде...

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

}

Теперь этот класс генерирует только одну ветвь, чтобы создать дерево, вам понадобятся два экземпляра этого класса с разными deltas

Далее нам нужно каким-то образом попросить этот генератор сгенерировать следующий шаг на регулярной основе. Для меня это обычно вызывает использование Swing Timer.

Причина была:

  • Это просто. Серьезно, это очень просто
  • Он не будет блокировать EDT, поэтому не замораживает пользовательский интерфейс.
  • Он обновляется в контексте EDT, что позволяет безопасно обновлять состояние пользовательского интерфейса изнутри.

Объединив эти две вещи в простой JPanel, который управляет Timer и рисует точки...

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;
        }
    }
}
person MadProgrammer    schedule 30.11.2017