Найти в Дзене

Рисуем фракталы с помощью PHP и Cairo. Часть 1. Основы фрактальной графики

Оглавление

В этой статье мы узнаем что такое фракталы и как рисовать их на 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(); // запускаем наш фреймворк в работу

?>@

После запуска этого кода мы увидим что-то вроде:

-2

Это двумерная версия фрактала «Канторова пыль», который в классическом варианте встречается в области цифровой обработки сигналов (ЦОС) при решении задачи устранения дискретного шума.

Здесь мы используем свойство рекурсии, о котором мы говорили ранее в предыдущих статьях. Это свойство позволяет сделать наш код наглядным и простым и сфокусироваться на самой отрисовке. Алгоритм отрисовки достаточно прост: мы берём точку слева и рисуем прямоугольник в треть длины всего отрезка сверху, а затем берём точку справа и рисуем такой же отрезок, и после повторяем эту операцию для левой и правой части, погружаясь на всё глубже по уровню рекурсии.

Для того, чтобы нарисовать прямоугольник, нам достаточно всего трёх методов вызываемых из объекта С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();

?>

На выходе нашей программы мы увидим следующее изображение:

-4

Этот фрактал также называется «Дракон Парка Юрского периода», так как он был приведён в книге американского фантаста Майкла Крайтона «Парк Юрского периода» в качестве иллюстрации непредсказуемости последствий поведения сложной системы. В этом произведении непроверенная компьютерная система оказывается не в состоянии контролировать популяцию животных с непредсказуемой моделью поведения, что приводит к разрушительным последствиям (теория хаоса в действии).

Семь раз отмерь, один раз отрежь
Русская народная поговорка

Закон Мёрфи призван для того, чтобы исключить всякую возможность подобного сценария. В мире программирования он выполняет очень важную роль, и его нужно всегда учитывать. И особенно его нужно учитывать, когда разрабатываемое программное обеспечение выполняет функции управления сложной техникой, от которой зависят человеческие жизни. На практике это означает более тщательный подход к разработке и тестированию программ.

В нашей статье мы рассмотрели различные виды плоских фракталов и способы их отрисовки с помощью графической библиотеки Cairo и фреймворка GTK на PHP. На этом тема фракталов и векторной графики не заканчивается. И мы более подробно раскроем её в следующей статье, которая будет посвящена фрактальной геометрии природы и грамматикам Линденмайера.

Можно ли стать программистом за год с нуля?
Читайте в моей бесплатной мини-книге «Путь в программисты». Скачать её можно здесь.