Как я уже упоминал, я начинающий (и самый скромный во Вселенной) программист на 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)


  1. SerafimArts
    07.10.2015 18:50

    Вопрос по теме, но к сообществу. Увидел класс Point2D отсюда и вопрос, т.к. использую для этих целей имя «Vector»:

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


    1. HaruAtari
      07.10.2015 19:24

      По факту ведь реализация (методы и поля) у них одинаковая совершенно, так ведь?

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


      1. SerafimArts
        07.10.2015 19:44

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

        В любом случае всегда можно написать class Point2 extends Vector2 {}.


        1. EndUser
          07.10.2015 20:39
          +3

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

          Вообразите себе время:
          Есть «моментальный» тип, измеряемый в минутах-месяцах от фиксированного начала календаря. «Момент» с «моментом» не складывается, хотя можно найти разность моментов (это будут данные интервального типа).
          Есть «интервальный» тип, измеряемый так же минутах-месяцах, но не имеющий привязки к фиксированному началу. Можно сложить интервал с интервалом, найти их разницу — в результате будут интервалы. Если сложить интервал с моментом, на выходе будет момент. Очевидно, что интервал сам по себе датой быть не может.


          1. SerafimArts
            07.10.2015 21:04

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


            1. EndUser
              07.10.2015 21:19
              +1

              Так у вас и валюта в вещественном типе хранится, и даже масса тоже в вещественном. И хэндлеры в целочисленном, арифметически совместимом, например, со счётчиками.

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

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

              Я не знаю объектной модели Java, но на вашем месте я бы внимательно читал документы на предмет именно такой «семантической» (я называю «бизнес», потому, что мало ли где ещё эта семантика) разницы между точкой и вектором.


    1. DanmerZ
      07.10.2015 20:08
      -1

      В геометрии нет определения точки, но можно схитрить, «определив» точку как конец вектора (x,y,z)


      1. EndUser
        07.10.2015 21:26

        Мдя…


        1. DanmerZ
          07.10.2015 21:41

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


          1. EndUser
            07.10.2015 21:51

            Поясню на всякий случай.

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

            Если вам понадобились батарейки, вы что-то сделали не так.


            1. DanmerZ
              07.10.2015 22:06

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


              1. EndUser
                07.10.2015 22:28

                Место и скорость тоже взаимозаменяемы как типы данных ;-)


  1. SirEdvin
    07.10.2015 19:34

    Прошу прощение, но ведь уже есть класс java.awt.geom.Point2D, которые специально используются для всех подобных классов (например, точки в Line2D возвращаются в виде java.awt.geom.Point2D), зачем нужно вводить свои точки?

    Тогда, если я не ошибаюсь, и необходимость в Branch отпала бы.


    1. Snakecatcher
      07.10.2015 22:08

      Класс Point2D, я взял из другого своего проекта. Дополнительно в нем были определен метод для вращения точки относительно оси координат. И ввиду того, что я еще не гуру в Java, стараюсь изобретать велосипеды, для получения дополнительной практики.
      Отдельный класс Branch был заведен из соображений расширяемости. Например для каждой ветки можно сохранять цвет. В методе getShape можно отрисовывать не линию, а полигон.


  1. potapuff
    07.10.2015 20:20
    +1

    А зачем в Branch длинна?


    1. Snakecatcher
      07.10.2015 22:12

      Она была введена, условно говоря «с запасом». На данный момент используется для раскраски ветвей и «листьев», в разые цвета. Обратите внимание в классе PaintPanel, закомментированный участок кода:

      if(b.length>30)
      g2d.setColor(Color.ORANGE.darker());
      else
      g2d.setColor(Color.GREEN);
      

      Всё, что длиннее 30 — темно-оранжевое, меньше — зелёный.


      1. SirEdvin
        07.10.2015 23:47

        Хм… а не пробовали свернуть так:

        g2d.setColor(b.length>30 ? Color.ORANGE.darker() : Color.GREEN);
        


        Только тогда желательно будет выделить под Color.ORANGE.darker() отдельную переменную, что бы он не считался каждый раз.


        1. Snakecatcher
          08.10.2015 00:19

          Красивое решение. Спасибо, буду иметь в виду.
          Сейчас мне кажется, лучшим решением, объявить переменную типа Color в классе Branch, и при создании определять цвет в зависимости от количества шагов рекурсии. Как вы считаете?


  1. ostapbender
    08.10.2015 18:53

    1. Snakecatcher
      09.10.2015 10:27

      Мой английский желает желать лучшего. :( Можете вкратце рассказать о чем статья?