В этой статье мы узнаем что такое фракталы и как рисовать их на PHP используя GTK и Cairo.
Немного о фракталах. Интересные факты
Фрактал — геометрическая фигура обладающая свойством самоподобия. Слово «фрактал» является производным от латинского слова fractus, что означает дробный. Этим свойством обладают практически все объекты природы — начиная с гигантских скоплений Галактик и заканчивая элементарными частицами такими как кварки.
Галактики, рельефы планет, океанские волны, облака и молнии, реки, формы растений и животных, и даже человеческое тело можно рассматривать как фракталы…
Бенуа Мандельброт (на фото) — отец основатель фрактальной геометрии, бунтарь среди математиков, впервые подробно описал этот термин в книге «Фрактальная геометрия природы», изданной в 1977 году. Эта книга в значительной степени повлияла на развитие компьютерной графики, так представила простой способ для генерации сложных геометрических объектов, таких как горы, облака и растения, необходимых для создания фотореалистичных сцен.
Если бы эта книга не попала в нужный момент в руки Лорена Карпетнера — сооснователя анимационной компании Pixar, может мы бы и не увидели всей красоты декораций таких фильмов как «Властелин колец»,»Аватар», «2012», «Матрица» и прочих. При создании этих фильмов использовался алгоритм Reyes rendering — алгоритм, который по утверждению самого автора «может отрисовать всё что ты когда-либо видел».
Библиотека векторной графики Cairo
Итак, мы начинаем рассмотрение основ фрактальной графики на PHP. В нашей работе мы будем использовать графическую библиотеку Cairo, предназначенную для отрисовки векторной графики. Эта библиотека написана на языке C и может быть использована в связке с такими языками как C++, Python и др.
Данная библиотека обладает тремя существенными преимуществами, обеспечивающими её популярность в среде open source:
- открытый исходный код
- переносимость графического кода между различными платформами
- качественная отрисовка векторной графики
Мы так же будем использовать фреймворк GTK, чтобы иметь возможность взаимодействовать с нашим кодом через графический интерфейс и просматривать получившиеся изображения.
Создание окна GTK. Подготовительный этап
Для начала нам придётся создать класс, который будет отвечать за создание окна. Мы назовём его FractalDrawingWindow и унаследуем его от базового класса GtkWindow, который входит во фреймворк GTK. Это позволит нам работать с оконной системой нашей операционной системы и отображать сгенерированные изображения фракталов внутри неё.
<?php
abstract class FractalDrawingWindow extends GtkWindow {
// глубина рекурсии создаваемого фрактального изображения (по умолчанию)
protected $recursionDepth = 5;
// конструктор класса
public function FractalDrawingWindow() {
parent::__construct(); // здесь мы вызываем конструктор базового класса,
// для успешной инициализации окна
$this->set_title($this->getName()); // устанавливаем название окна
// по имени фрактала
// устанавливаем корректный способ выхода из окна
$this->connect_simple('destroy', array('gtk', 'main_quit'));
$drawingArea = new GtkDrawingArea(); // создаём область для рисования
// устанавливаем функцию отображения фрактала для данной области рисования
$drawingArea->connect('expose_event',array($this,'onExpose'));
// добавляем созданную область в наше окно
$this->add($drawingArea);
// устанавливаем размер окна
$this->set_default_size(640,480); // по ширине и высоте
// устанавливаем позицию окна на экране
$this->set_position(GTK::WIN_POS_CENTER);
// включаем отображение окна
$this->show_all(); // здесь начинается запуск алгоритма отрисовки
// и отображения фрактала
}
// функция экспозиции фрактала
public function onExpose($darea, $event){
$context = $darea->window->cairo_create(); // создаём Cairo-контекст для отрисовки
$this->onDraw($context); // отрисовываем конкретный вид фрактала
}
// функция установа глубины рекурсии
public function setRecursionDepth($recursionDepth){
// здесь мы ограничиваем глубину рекурсии для избежания segmentation fault
if($recursionDepth >= 0 && $recursionDepth <= 22){
$this->recursionDepth = $recursionDepth;
}
}
// мы используем чистые абстрактные методы, чтобы
abstract public function getName(); // определить названия фракталов
abstract protected function onDraw($context); // и методы их отрисовки
// в классах-потомках
}
?>
Созданный нами класс представляет собой абстрактный класс, предоставляющий возможность использования окна GTK для отрисовки. Но данный класс не предполагает что именно мы будем рисовать в этом окне, поэтому нам нужно создать для него классы-потомки, которые будут определять конкретный вид функций получения имени окна и способа отрисовки фрактала. Назовём файл с описанием нашего класса как «drawing_window.php», чтобы иметь возможность подключать его в дальнейшем при создании классов-потомков.
Рисуем фракталы с помощью PHP и Cairo
Теперь мы можем приступать к рассмотрению конкретных видов фракталов и способов их рисования. И начнём мы с фрактала, который получил название в честь немецкого математика, Георга Кантора.
Канторова пыль
Ashes to ashes, dust to dust
Фраза из английской похоронной службы, часто используемая для обозначения фатальной неизбежности конца.
Рассмотрим следующий код:
<?php
include 'drawing_window.php'; // здесь мы подключаем файл с ранее созданным
// классом для работы с окнами GTK
// мы создаём класс по конкретному виду фрактала и унаследуем его от класса
// FractalDrawingWindow, капсулирующего функции для работы с оконной системой
class CantorDust extends FractalDrawingWindow {
// здесь мы определяем функцию, которая будет отображаться в названии окна
public function getName() {
return "Cantor dust fractal";
}
// здесь мы определяем метод отрисовки фрактала
protected function onDraw($context){
// устанавливаем основной цвет для отрисовки
$context->setSourceRgb(0.5, 0.5, 0.5);
// запускаем рекурсивную функцию отрисовки фрактала "Канторова пыль"
$this->draw($context,$this->recursionDepth, 0, 0, $this->get_size()[0], floatval($this->get_size()[1]) / $this->recursionDepth);
}
// рекурсивная функция отрисовки
public function draw($context,$level, $posX, $posY, $sizeX, $sizeY){
// в параметрах мы передаём начальные позиции по оси X и Y и размеры элементов для каждого шага рекурсии
// по достижении установленной глубины рекурсии, производим выход из функции отрисовки
if($level == 0){
return;
}
// вычисляем новый размер и положение элемента по оси X
$newSizeX = $sizeX / 3;
$newPosX = $posX + 2 * $newSizeX;
// рисуем левый прямоугольник
$context->rectangle($posX,$posY,$newSizeX,$sizeY); // устанавливаем параметры
$context->fill(); // делаем заливку выделенным цветом, уставновленным ранее в методе onDraw()
// рисуем правый прямоугольник
$context->rectangle($newPosX,$posY,$newSizeX,$sizeY);
$context->fill();
// запускаем отрисовку следующего уровня рекурсии
$this->draw($context, $level - 1, $posX, $posY + $sizeY, $newSizeX, $sizeY); // слева
$this->draw($context, $level - 1, $newPosX, $posY + $sizeY, $newSizeX, $sizeY); // и справа
}
}
// Тест
$fractal = new CantorDust(); // создаём объект класса для нашего фрактала
$fractal->setRecursionDepth(15); // устанавливаем глубину рекурсии
Gtk::main(); // запускаем наш фреймворк в работу
?>@
После запуска этого кода мы увидим что-то вроде:
Это двумерная версия фрактала «Канторова пыль», который в классическом варианте встречается в области цифровой обработки сигналов (ЦОС) при решении задачи устранения дискретного шума.
Здесь мы используем свойство рекурсии, о котором мы говорили ранее в предыдущих статьях. Это свойство позволяет сделать наш код наглядным и простым и сфокусироваться на самой отрисовке. Алгоритм отрисовки достаточно прост: мы берём точку слева и рисуем прямоугольник в треть длины всего отрезка сверху, а затем берём точку справа и рисуем такой же отрезок, и после повторяем эту операцию для левой и правой части, погружаясь на всё глубже по уровню рекурсии.
Для того, чтобы нарисовать прямоугольник, нам достаточно всего трёх методов вызываемых из объекта Сairo-context — это:
- rectangle — устанавливающий параметры прямоугольника,
- fill — производящий заливку цветом,
- а так же setSourceRgb — определяющий цвет заливки.
Вообще существует большущий список методов для работы с Cairo-context, так что предлагаю ознакомиться с ним самостоятельно. В дальнейшем мы будем использовать методы из этого списка для отрисовки всё более сложных и красивых фракталов.
Ура, теперь можно сделать перерыв и выпить чашечку чая! Только что мы разобрались в том, как рисовать простейший фрактал используя PHP и Cairo!
Теперь рассмотрим пример посложнее.
Дерево Пифагора
"Пифагоровы штаны на все стороны равны. Чтобы это доказать, нужно снять и показать"
Народное творчество
Этот фрактал придумал немецкий учитель математики Альберт Босман в 1942 году и назвал его в честь древнегреческого математика Пифагора, потому как каждый уровень этого фрактала содержит фигуры, которые традиционно используются для доказательства теоремы Пифагора: три соприкасающихся квадрата, содержащих между собой прямоугольный треугольник.
Построение данного фрактала начинается с квадрата, над которым строятся ещё два квадрата, уменьшенных на величину √2/2, попарно соединённых между собой общими углами. В классическом случае, угол между двумя квадратами близлежащих уровней составляет 45°, тогда как существуют различные вариации данного фрактала:
- обнажённое дерево Пифагора — каждый квадрат заменяется отрезком
- обдуваемое ветром дерево Пифагора — углы слева и справа отличаются от 45°
- обдуваемое ветром обнажённое дерево Пифагора — сочетаются первые два пункта
Мы же рассмотрим дерево Пифагора в классическом варианте. Вот его код:
<?php
include 'drawing_window.php'; // как и ранее мы подключаем класс FractalDrawingWindow
// от которого мы унаследуем методы для работы с оконной системой
class PythagorianTree extends FractalDrawingWindow {
// определим метод для отображения имени фрактала в заголовке окна
public function getName() {
return "Pythagorian tree fractal";
}
// и метод для отрисовки самого фрактала
protected function onDraw($context){
$context->setSourceRgb(0.4, 0.9, 0.4); // определим базовый цвет отрисовки
// запустим рекурсивную функцию отрисовки нашего фрактала
$this->draw($context, $this->recursionDepth, $this->get_size()[0]/2 - 50, $this->get_size()[1], $this->get_size()[0]/2 +50, $this->get_size()[1]);
}
// рекурсивная функция отрисовки
public function draw($context, $depth, $x1, $y1, $x2, $y2){
// производим ограничение на глубину рекурсии
if($depth == 0) {
return;
}
// вычисляем относительные смещения точек квадрата по осям X и Y
$dx = $x2-$x1;
$dy = $y1-$y2;
// изменяем координаты точек квадрата на данном уровне рекурсии
$x3 = $x2 - $dy;
$y3 = $y2 - $dx;
$x4 = $x1 - $dy;
$y4 = $y1 - $dx;
$x5 = $x4 + floatval(($dx - $dy))*0.5;
$y5 = $y4 - floatval(($dx + $dy))*0.5;
// рисуем квадрат
$context->MoveTo($x1, $y1); // смещаемся к координате (x1,y1) это первая точка
$context->LineTo($x2, $y2); // создаём отрезок из точки (x1,y1) к точке (x2,y2)
$context->LineTo($x3, $y3); // и т.д.
$context->LineTo($x4, $y4); // до последней вычисленной точки
$context->closePath(); // закрываем линию возвращаясь в исходную точку (x1,y1)
// устанавливаем цвет на новом уровне рекурсии
$context->setSourceRgb(1.0 - floatval($this->recursionDepth-$depth)/$this->recursionDepth, floatval($this->recursionDepth-$depth)/$this->recursionDepth, 0.0);
// делаем заливку цветом
$context->fill();
$depth--; // декрементируем счётчик уровня рекурсии
$this->draw($context, $depth, $x4, $y4, $x5, $y5); // рисуем квадрат слева
$this->draw($context, $depth, $x5, $y5, $x3, $y3); // рисуем квадрат справа от исходного
}
}
// Тест
$fractal = new PythagorianTree(); // создание объекта окна GTK с фракталом
$fractal->setRecursionDepth(10); // установ уровня рекурсии
Gtk::main(); // запуск фреймворка
?>
Чудесно! Теперь мы видим, что можно создавать достаточно сложные фигуры сравнительно простыми методами, используя свойства рекурсии и графические примитивы библиотеки Cairo!
Здесь мы используем несколько иную технику для отрисовки прямоугольников, нежели чем в предыдущем примере. Сочетание функций moveTo, lineTo и closePath позволяет нам строить любые замкнутые многогранники. Заливка цветом при этом ничем не отличается от предыдущего случая. Мы так же указываем значения интенсивности для красного R, зелёного G и синего B каналов, передавая их в качестве параметров в функцию setSourceRgb, а затем вызываем функцию fill.
Ну и напоследок я хотел бы рассмотреть следующий фрактал.
Дракон Хартера — Хейтуэя
Всё, что может пойти не так, пойдет не так
Закон Мёрфи
Дракон Хратера — Хейтуэя, так же известный как дракон Хартера был впервые представлен в 1967 году Мартином Гарднером в журнале «Scientific American». Своим названием он обязан Джону Хейтуэю и Вильяму Хартеру — двум физикам из NASA, впервые исследовавшим этот фрактал. К сожалению имя третьего физика — Брюса Бэнкса обычно опускают и вместо него ставят прочерк (некоторые люди к сожалению особенно подвержены действию закона Мёрфи).
Итак, рассмотрим следующий код:
<?php
include 'drawing_window.php';
class DragonCurve extends FractalDrawingWindow {
public function getName() {
return "Dragon curve fractal";
}
protected function onDraw($context){
// здесь в отличие от предыдущих двух случаев мы используем итеративный метод
// создания последовательности операций отрисовки
$turns = $this->getSequence($this->recursionDepth);
$startAngle = -$this->recursionDepth * 3.14 / 4;
$side = 400 / pow(2, $this->recursionDepth / 2.);
$this->draw($context, $turns, $startAngle, $side, $this->get_size()[0]/2, $this->get_size()[1]/2);
}
// метод получения последовательности операций отрисовки дракона Хартера
public function getSequence($depth){
$seq = array(); // создаём исходный пустой массив
for($i =0; $i < $depth; $i++) {
$copy = $seq; // создаём копию данного массива
$copy = array_reverse($copy); // инвертируем порядок элементов массива на инверсный (элементы в начале перемещаются в конец и наоборот)
// добавляем к исходному массиву код направления угла поворота линии для данного фрактала
array_push($seq,1);
// добавляем к исходному массиву реверсную копию инвертированных направлений углов поворота с предыдущего уровня итерации
foreach($copy as $val){
array_push($seq,-$val);
}
}
// возвращаем последовательность направлений углов поворота линий данного фрактала
return $seq;
}
// определяем функцию отрисовки для полученной последовательности направлений
public function draw($context, $turns, $startAngle, $side, $x1, $y1){
$angle = $startAngle; // устанавливаем начальный угол
$x2 = $x1 + intval(cos($angle)*$side); // производим поворот исходной точки
$y2 = $y1 + intval(sin($angle)*$side);
$context->setSourceRgb(0.4, 0.4, 0.9); // устанавливаем исходный цвет линии
// переходим к точке x1,y1
$context->moveTo($x1,$y1);
// строим отрезок к точке x2,y2
$context->lineTo($x2,$y2);
// производим отрисовку
$context->stroke();
// переходим к следующей точке
$x1 = $x2;
$y1 = $y2;
foreach($turns as $turn){
$angle += $turn * 3.14 / 2; // изменяем угол поворота на 90 градусов в соответствии
// с рассчитанной заранее последовательностью
// производим поворот и отрисовку фигуры на новом уровне итерации
$x2 = $x1 + intval(cos($angle)*$side);
$y2 = $y1 + intval(sin($angle)*$side);
$context->moveTo($x1,$y1);
$context->lineTo($x2,$y2);
$context->stroke();
$x1 = $x2;
$y1 = $y2;
}
}
}
// Тест
$fractal = new DragonCurve();
$fractal->setRecursionDepth(15);
Gtk::main();
?>
На выходе нашей программы мы увидим следующее изображение:
Этот фрактал также называется «Дракон Парка Юрского периода», так как он был приведён в книге американского фантаста Майкла Крайтона «Парк Юрского периода» в качестве иллюстрации непредсказуемости последствий поведения сложной системы. В этом произведении непроверенная компьютерная система оказывается не в состоянии контролировать популяцию животных с непредсказуемой моделью поведения, что приводит к разрушительным последствиям (теория хаоса в действии).
Семь раз отмерь, один раз отрежь
Русская народная поговорка
Закон Мёрфи призван для того, чтобы исключить всякую возможность подобного сценария. В мире программирования он выполняет очень важную роль, и его нужно всегда учитывать. И особенно его нужно учитывать, когда разрабатываемое программное обеспечение выполняет функции управления сложной техникой, от которой зависят человеческие жизни. На практике это означает более тщательный подход к разработке и тестированию программ.
В нашей статье мы рассмотрели различные виды плоских фракталов и способы их отрисовки с помощью графической библиотеки Cairo и фреймворка GTK на PHP. На этом тема фракталов и векторной графики не заканчивается. И мы более подробно раскроем её в следующей статье, которая будет посвящена фрактальной геометрии природы и грамматикам Линденмайера.
Можно ли стать программистом за год с нуля?
Читайте в моей бесплатной мини-книге «Путь в программисты». Скачать её можно здесь.