Фрактальный кустик от новичка для новичков

    Как я уже упоминал, я начинающий (и самый скромный во Вселенной) программист на Java. Иногда душа просит чего-то такого, вот прям чтоб сначала развернулась, а потом опять свернулась. Красоты хочется. А красоту рисовать мы еще не обучены. Зато обучены рисовать палочки и кружочки.

    И с боевым кличем «Красота в простоте!», рисуем из палочек. А что мы можем нарисовать красивое и простое, да чтоб коллеги ахнули в восторге? И тут на помощь приходит красивое слово – Фрактал.

    Сначала определение: «Фрактал – это структура из частей… бла-бла-бла… самоподобие… бла-бла-бла… красиво… бла-бла-бла...».



    И вооруженные этим исчерпывающим знанием давайте нарисуем фрактальное дерево, а точнее кустик.

    Для начала создадим оболочку откуда будем всё запускать:

    Класс FractalTreeTest
    public class FractalTreeTest {
    
    	public static void main(String[] args) {
    		EventQueue.invokeLater(new Runnable() {		
    			public void run() {
    				JFractalFrame frame = new JFractalFrame();
    				frame.setVisible(true);
    			}
    		});
    	}
    }
    


    Теперь создадим класс, отвечающий за вывод на экран окна и запуск анимации:

    Класс JFractalFrame
    class JFractalFrame extends JFrame{
    	boolean startAnimation=false;
    	JPanel paintPanel;
    	
    	public JFractalFrame() {
    		setTitle("FractalTree");
    		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    		paintPanel = new PaintPanel();
    		paintPanel.setBackground(Color.DARK_GRAY);
    		add(paintPanel);
    		requestFocus();//Обращаем фокус на наш фрейм. Без этой строки, реакции не будет
    		// А здесь отлавливаем любое нажатие на кнопку, и запускаем анимацию
    		addKeyListener(new KeyAdapter() {
    			public void keyPressed(KeyEvent e){
    				if(e.getKeyCode()>0 && !startAnimation){
    					pack();
    					startAnimation=true;
    					startAnimation ();
    				}
    			}
    		});
    		paintPanel.repaint();
    		pack();
    	}
    	
    	public void startAnimation(){
    		Runnable galaxyRun = new FractalRunnable((PaintPanel) paintPanel);
    		Thread t = new Thread(galaxyRun);
    		t.start();
    	}
    }
    


    Класс с панелью, на которой отрисовывается все действие. Именно в ней содержится рекурсивная функция fractal, отрисовывающая новыве ветки:

    Класс PaintPanel
    class PaintPanel extends JPanel{
    	List<Branch> listBranch =  new CopyOnWriteArrayList<>();
    	double angle=0;
    		
    	public void setAngle(double angle) {
    		this.angle = angle;
    	}
    	//Рекурсивная функция, в которой отрисовываются две новые ветки исходящие из предыдущей, и добавляются в список
               //в неё передается длина ветки, точка начала отрисовки, угол наклона, и шаг рекурсии
    	public void fractal(int startLength, Point2D startPoint, double alpha, int step){
    		if(alpha<0) alpha=360;
    		double radian =(alpha/(180/Math.PI));
    		Point2D endPoint1 = new Point2D();
    		Point2D endPoint2 = new Point2D();
    				
    		endPoint1.setX((float) (startPoint.getX()-startLength*Math.cos(radian)));
    		endPoint1.setY((float) (startPoint.getY()-startLength*Math.sin(radian)));
    		addBranch(new Branch(startPoint, endPoint1, startLength));
    		
    		endPoint2.setX((float) (startPoint.getX()-startLength*Math.cos(radian)));
    		endPoint2.setY((float) (startPoint.getY()-startLength*Math.sin(radian)));
    		addBranch(new Branch(startPoint, endPoint2, startLength));
    		
    		if(step>0){
    			step--;
    			startLength-=4; //уменьшаем длину ветки
    			//попробуйте поэкспериментировать в следующих строках со знаками и числами. Можете 
                                       //получить интересные варианты.
    			fractal(startLength, endPoint1, alpha-(20+angle),step); //angle понадобится для анимации
    			fractal(startLength, endPoint2, alpha+(20-angle), step);	
    		}
    	}
    	
    	public void addBranch(Branch b){
    		listBranch.add(b);
    	}
    	
    	public void paintComponent(Graphics g){
    		super.paintComponent(g);
    //Отрисовываем в середине экрана, с начальной длиной ветки 60, углом 90, и на 10 шагов
    		fractal(60, new Point2D(320, 480), 90, 10);
    		Random randomX = new Random();
    		Graphics2D g2d = (Graphics2D)g;
    		for(Branch b: listBranch){
    		// Можно отрисовывать так, но получится «сумасшедшее» дискотечное дерево	
    g2d.setColor(new Color(randomX.nextInt(255),randomX.nextInt(255),randomX.nextInt(255)));
    		//Если закомментировать предыдущую строку, и раскомментировать следующий код, 
                   //то ствол, ветви и листья будут отрисовываться разным цветом
    			/*
    			if(b.length>30)
    				g2d.setColor(Color.ORANGE.darker());
    			else
    				g2d.setColor(Color.GREEN);
    				*/
    			g2d.draw(b.getShape());
    			
    		}
    	//после отрисовки очищаем список, чтобы можно было принять новый
    		listBranch.clear();
    	}
    //задаем размер панели отрисовки
    	public Dimension getPreferredSize() {
    		// TODO Auto-generated method stub
    		return new Dimension(640,480);
    	}	
    }
    


    Класс двумерной точки:

    Класс Point2D
    class Point2D{
    	
    private float x, y;
    	//создаем точку по двум координатам
    	public Point2D(float x, float y) {
    		this.x=x;
    		this.y = y;
    	}
    	
    	public Point2D(){
    		
    	}
    	
    	public void setX(float x) {
    		this.x = x;
    	}
    	
    	public void setY(float y) {
    		this.y = y;
    	}
    	public float getX() {
    		return x;
    	}
    	public float getY() {
    		return y;
    	}
    
    }
    


    Класс отвечающий за отрисовку ветки:

    Класс Branch
    class Branch{
    	Point2D begin;
    	Point2D end;
    	int length;
    	//Строим ветку по двум точкам	
    	public Branch(Point2D begin, Point2D end, int length) {
    		this.begin=begin;
    		this.end=end;
    		this.length=length;
    	}
    	//рисуем прямую линию по заданным координатам
    	public Line2D getShape(){
    		return new Line2D.Double(begin.getX(), begin.getY(), end.getX(), end.getY());
    	}
    	
    }
    


    И наконец класс Runnable, в котором будет крутиться наша анимация:

    Класс FractalRunnable
    class FractalRunnable implements Runnable{
    	PaintPanel paintPanel;
    	
    	public FractalRunnable(PaintPanel paintPanel) {
    		// TODO Auto-generated constructor stub
    		this.paintPanel=paintPanel;
    	}
    	
    	public void run() {
    		double count=0;
    		boolean leftDir = true;
    		while(true){
    		//»Что стоишь качаясь, до самого тына…» С.Есенин
    		//Высота тына регулируется переменной count
    			if(count>8 && a<count){
    				leftDir=false;
    			}
    			
    			if(count<-8 && count>-9){
    				leftDir=true;
    				}
    			if(leftDir)
    				count+=0.01;
    			else
    				count-=0.01;
    				
    			paintPanel.setAngle(a);
    			paintPanel.repaint();
    			try {
    				Thread.sleep(5);
    			} catch (InterruptedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
    		}
    		
    	}
    }
    


    Запускаем, нажимаем на любую кнопку на клавиатуре, и радуемся.

    Засим позвольте откланяться.
    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 20
    • 0
      Вопрос по теме, но к сообществу. Увидел класс Point2D отсюда и вопрос, т.к. использую для этих целей имя «Vector»:

      Как правильно именовать точки в пространстве — с помощью класса Point или с помощью Vector. По факту ведь реализация (методы и поля) у них одинаковая совершенно, так ведь?
      • 0
        По факту ведь реализация (методы и поля) у них одинаковая совершенно, так ведь?

        Вы рассуждаете опираясь на реализацию, а не на интерфейсы. Важно то, что вы работаете с точками, а не векторами. Поэтому быдут логично использовать объекты Point, а не Vector. Но это, на мой взгляд, больше идеалогический вопрос.
        • 0
          Семантику я сейчас опускаю, интересуют подводные камни и общепринятость использования объекта вектора как точки.

          В любом случае всегда можно написать class Point2 extends Vector2 {}.
          • +3
            Точка подразумевает, что она описывается вектором, опирающимся на начало выбранной системы координат (0; 0).
            Идея вектора не подразумевает фиксированного начала. Вектор как «смещение», оператор, который можно применить к чему угодно.

            Вообразите себе время:
            Есть «моментальный» тип, измеряемый в минутах-месяцах от фиксированного начала календаря. «Момент» с «моментом» не складывается, хотя можно найти разность моментов (это будут данные интервального типа).
            Есть «интервальный» тип, измеряемый так же минутах-месяцах, но не имеющий привязки к фиксированному началу. Можно сложить интервал с интервалом, найти их разницу — в результате будут интервалы. Если сложить интервал с моментом, на выходе будет момент. Очевидно, что интервал сам по себе датой быть не может.
            • 0
              Но если брать реализации — я не видел, что бы вектор описывался двумя точками — Unreal Engine 4 (включая ThirdParty/Oculus/LibOVR и ThirdParty/nvTextureTools/nvTextureTools-2.0.6, может быть ещё что), Libgdx (com/badlogic/gdx/math/Vector3) и предполагаю что прочие по умолчанию подразумевают начало [0,0,0]. Из чего можно сделать вывод, что речь идёт опять же о семантических отличиях вектора от точки, а не отличиях в реализации. Вопрос был конкретно в верности использования реализации вектора, как точек для координатной сетки мешей, мира, экторов и прочего.
              • +1
                Так у вас и валюта в вещественном типе хранится, и даже масса тоже в вещественном. И хэндлеры в целочисленном, арифметически совместимом, например, со счётчиками.

                Именно поэтому возникла «венгерская нотация» — предварять наименование переменной её бизнес-типом. Если верно пользоваться венгерской нотацией, то программисту становится яснее что масса+валюта=неудачная мысль, даже если компилятор ошибок не выдает.

                Повторяю:
                момент + момент = операция бессмысленна
                момент — момент = интервал
                момент ± интервал = новый момент
                интервал ± интервал = интервал
                И:
                точка + точка = операция бессмысленна
                точка — точка = расстояние
                точка ± расстояние = новая точка
                расстояние ± расстояние = новое расстояние

                Я не знаю объектной модели Java, но на вашем месте я бы внимательно читал документы на предмет именно такой «семантической» (я называю «бизнес», потому, что мало ли где ещё эта семантика) разницы между точкой и вектором.
        • –1
          В геометрии нет определения точки, но можно схитрить, «определив» точку как конец вектора (x,y,z)
          • 0
            Мдя…
            • 0
              Поясню на всякий случай. Можно говорить о взаимнооднозначном соответствии векторов и точек в пространстве, если задана некоторая начальная точка. Но в этом случае, каждой точке в пространстве можно поставить в соответствие набор из координат вектора, который соответствует этой точке. И эти числа будут однозначно определять точку, как определяют вектор.
              • 0
                Поясню на всякий случай.

                https://pbs.twimg.com/media/CIbydaqWEAAgMmg.jpg

                Если вам понадобились батарейки, вы что-то сделали не так.
                • 0
                  Живая уточка тоже кушать требует. Точка и вектор вполне взаимозаменяемы как типы данных, всего лишь это хотел сказать.
                  • 0
                    Место и скорость тоже взаимозаменяемы как типы данных ;-)
        • 0
          Прошу прощение, но ведь уже есть класс java.awt.geom.Point2D, которые специально используются для всех подобных классов (например, точки в Line2D возвращаются в виде java.awt.geom.Point2D), зачем нужно вводить свои точки?

          Тогда, если я не ошибаюсь, и необходимость в Branch отпала бы.
          • 0
            Класс Point2D, я взял из другого своего проекта. Дополнительно в нем были определен метод для вращения точки относительно оси координат. И ввиду того, что я еще не гуру в Java, стараюсь изобретать велосипеды, для получения дополнительной практики.
            Отдельный класс Branch был заведен из соображений расширяемости. Например для каждой ветки можно сохранять цвет. В методе getShape можно отрисовывать не линию, а полигон.
          • +1
            А зачем в Branch длинна?
            • 0
              Она была введена, условно говоря «с запасом». На данный момент используется для раскраски ветвей и «листьев», в разые цвета. Обратите внимание в классе PaintPanel, закомментированный участок кода:
              if(b.length>30)
              g2d.setColor(Color.ORANGE.darker());
              else
              g2d.setColor(Color.GREEN);
              

              Всё, что длиннее 30 — темно-оранжевое, меньше — зелёный.
              • 0
                Хм… а не пробовали свернуть так:
                g2d.setColor(b.length>30 ? Color.ORANGE.darker() : Color.GREEN);
                


                Только тогда желательно будет выделить под Color.ORANGE.darker() отдельную переменную, что бы он не считался каждый раз.
                • 0
                  Красивое решение. Спасибо, буду иметь в виду.
                  Сейчас мне кажется, лучшим решением, объявить переменную типа Color в классе Branch, и при создании определять цвет в зависимости от количества шагов рекурсии. Как вы считаете?
              • 0
                Мой английский желает желать лучшего. :( Можете вкратце рассказать о чем статья?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое