И с боевым кличем «Красота в простоте!», рисуем из палочек. А что мы можем нарисовать красивое и простое, да чтоб коллеги ахнули в восторге? И тут на помощь приходит красивое слово – Фрактал.
Сначала определение: «Фрактал – это структура из частей… бла-бла-бла… самоподобие… бла-бла-бла… красиво… бла-бла-бла...».
И вооруженные этим исчерпывающим знанием давайте нарисуем фрактальное дерево, а точнее кустик.
Для начала создадим оболочку откуда будем всё запускать:
public class FractalTreeTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
JFractalFrame frame = new JFractalFrame();
frame.setVisible(true);
}
});
}
}
Теперь создадим класс, отвечающий за вывод на экран окна и запуск анимации:
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, отрисовывающая новыве ветки:
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);
}
}
Класс двумерной точки:
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;
}
}
Класс отвечающий за отрисовку ветки:
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, в котором будет крутиться наша анимация:
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)
SirEdvin
07.10.2015 19:34Прошу прощение, но ведь уже есть класс java.awt.geom.Point2D, которые специально используются для всех подобных классов (например, точки в Line2D возвращаются в виде java.awt.geom.Point2D), зачем нужно вводить свои точки?
Тогда, если я не ошибаюсь, и необходимость в Branch отпала бы.Snakecatcher
07.10.2015 22:08Класс Point2D, я взял из другого своего проекта. Дополнительно в нем были определен метод для вращения точки относительно оси координат. И ввиду того, что я еще не гуру в Java, стараюсь изобретать велосипеды, для получения дополнительной практики.
Отдельный класс Branch был заведен из соображений расширяемости. Например для каждой ветки можно сохранять цвет. В методе getShape можно отрисовывать не линию, а полигон.
potapuff
07.10.2015 20:20+1А зачем в Branch длинна?
Snakecatcher
07.10.2015 22:12Она была введена, условно говоря «с запасом». На данный момент используется для раскраски ветвей и «листьев», в разые цвета. Обратите внимание в классе PaintPanel, закомментированный участок кода:
if(b.length>30) g2d.setColor(Color.ORANGE.darker()); else g2d.setColor(Color.GREEN);
Всё, что длиннее 30 — темно-оранжевое, меньше — зелёный.SirEdvin
07.10.2015 23:47Хм… а не пробовали свернуть так:
g2d.setColor(b.length>30 ? Color.ORANGE.darker() : Color.GREEN);
Только тогда желательно будет выделить под Color.ORANGE.darker() отдельную переменную, что бы он не считался каждый раз.Snakecatcher
08.10.2015 00:19Красивое решение. Спасибо, буду иметь в виду.
Сейчас мне кажется, лучшим решением, объявить переменную типа Color в классе Branch, и при создании определять цвет в зависимости от количества шагов рекурсии. Как вы считаете?
ostapbender
08.10.2015 18:53Snakecatcher
09.10.2015 10:27Мой английский желает желать лучшего. :( Можете вкратце рассказать о чем статья?
SerafimArts
Вопрос по теме, но к сообществу. Увидел класс Point2D отсюда и вопрос, т.к. использую для этих целей имя «Vector»:
Как правильно именовать точки в пространстве — с помощью класса Point или с помощью Vector. По факту ведь реализация (методы и поля) у них одинаковая совершенно, так ведь?
HaruAtari
Вы рассуждаете опираясь на реализацию, а не на интерфейсы. Важно то, что вы работаете с точками, а не векторами. Поэтому быдут логично использовать объекты Point, а не Vector. Но это, на мой взгляд, больше идеалогический вопрос.
SerafimArts
Семантику я сейчас опускаю, интересуют подводные камни и общепринятость использования объекта вектора как точки.
В любом случае всегда можно написать
class Point2 extends Vector2 {}
.EndUser
Точка подразумевает, что она описывается вектором, опирающимся на начало выбранной системы координат (0; 0).
Идея вектора не подразумевает фиксированного начала. Вектор как «смещение», оператор, который можно применить к чему угодно.
Вообразите себе время:
Есть «моментальный» тип, измеряемый в минутах-месяцах от фиксированного начала календаря. «Момент» с «моментом» не складывается, хотя можно найти разность моментов (это будут данные интервального типа).
Есть «интервальный» тип, измеряемый так же минутах-месяцах, но не имеющий привязки к фиксированному началу. Можно сложить интервал с интервалом, найти их разницу — в результате будут интервалы. Если сложить интервал с моментом, на выходе будет момент. Очевидно, что интервал сам по себе датой быть не может.
SerafimArts
Но если брать реализации — я не видел, что бы вектор описывался двумя точками — Unreal Engine 4 (включая
ThirdParty/Oculus/LibOVR
иThirdParty/nvTextureTools/nvTextureTools-2.0.6
, может быть ещё что), Libgdx (com/badlogic/gdx/math/Vector3
) и предполагаю что прочие по умолчанию подразумевают начало[0,0,0]
. Из чего можно сделать вывод, что речь идёт опять же осемантических
отличиях вектора от точки, а не отличиях вреализации
. Вопрос был конкретно в верности использованияреализации
вектора, как точек для координатной сетки мешей, мира, экторов и прочего.EndUser
Так у вас и валюта в вещественном типе хранится, и даже масса тоже в вещественном. И хэндлеры в целочисленном, арифметически совместимом, например, со счётчиками.
Именно поэтому возникла «венгерская нотация» — предварять наименование переменной её бизнес-типом. Если верно пользоваться венгерской нотацией, то программисту становится яснее что масса+валюта=неудачная мысль, даже если компилятор ошибок не выдает.
Повторяю:
момент + момент = операция бессмысленна
момент — момент = интервал
момент ± интервал = новый момент
интервал ± интервал = интервал
И:
точка + точка = операция бессмысленна
точка — точка = расстояние
точка ± расстояние = новая точка
расстояние ± расстояние = новое расстояние
Я не знаю объектной модели Java, но на вашем месте я бы внимательно читал документы на предмет именно такой «семантической» (я называю «бизнес», потому, что мало ли где ещё эта семантика) разницы между точкой и вектором.
DanmerZ
В геометрии нет определения точки, но можно схитрить, «определив» точку как конец вектора (x,y,z)
EndUser
Мдя…
DanmerZ
Поясню на всякий случай. Можно говорить о взаимнооднозначном соответствии векторов и точек в пространстве, если задана некоторая начальная точка. Но в этом случае, каждой точке в пространстве можно поставить в соответствие набор из координат вектора, который соответствует этой точке. И эти числа будут однозначно определять точку, как определяют вектор.
EndUser
Поясню на всякий случай.
https://pbs.twimg.com/media/CIbydaqWEAAgMmg.jpg
Если вам понадобились батарейки, вы что-то сделали не так.
DanmerZ
Живая уточка тоже кушать требует. Точка и вектор вполне взаимозаменяемы как типы данных, всего лишь это хотел сказать.
EndUser
Место и скорость тоже взаимозаменяемы как типы данных ;-)