В этой статье мы узнаем что такое фракталы и как рисовать их на PHP используя GTK и Cairo.
Немного о фракталах. Интересные факты
Фрактал — геометрическая фигура обладающая свойством самоподобия. Слово «фрактал» является производным от латинского слова fractus, что означает дробный. Этим свойством обладают практически все объекты природы — начиная с гигантских скоплений Галактик и заканчивая элементарными частицами такими как кварки.
Галактики, рельефы планет, океанские волны, облака и молнии, реки, формы растений и животных, и даже человеческое тело можно рассматривать как фракталы…
Бенуа Мандельброт (на фото) — отец основатель фрактальной геометрии, бунтарь среди математиков, впервые подробно описал этот термин в книге «Фрактальная геометрия природы», изданной в 1977 году. Эта книга в значительной степени повлияла на развитие компьютерной графики, так представила простой способ для генерации сложных геометрических объектов, таких как горы, облака и растения, необходимых для создания фотореалистичных сцен.
Если бы эта книга не попала в нужный момент в руки Лорена Карпетнера — сооснователя анимационной компании Pixar, может мы бы и не увидели всей красоты декораций таких фильмов как «Властелин колец», «Аватар», «2012», «Матрица» и прочих. При создании этих фильмов использовался алгоритм Reyes rendering — алгоритм, который по утверждению самого автора «может отрисовать всё что ты когда-либо видел».
Библиотека векторной графики Cairo
Итак, мы начинаем рассмотрение основ фрактальной графики на PHP. В нашей работе мы будем использовать графическую библиотеку Cairo, предназначенную для отрисовки векторной графики. Эта библиотека написана на языке C и может быть использована в связке с такими языками как C++, Python и др.
Данная библиотека обладает тремя существенными преимуществами, обеспечивающими её популярность в среде open source:
- открытый исходный код
- переносимость графического кода между различными платформами
- качественная отрисовка векторной графики
Мы так же будем использовать фреймворк GTK, чтобы иметь возможность взаимодействовать с нашим кодом через графический интерфейс и просматривать получившиеся изображения.
Создание окна GTK. Подготовительный этап
Для начала нам придётся создать класс, который будет отвечать за создание окна. Мы назовём его FractalDrawingWindow и унаследуем его от базового класса GtkWindow, который входит во фреймворк GTK. Это позволит нам работать с оконной системой нашей операционной системы и отображать сгенерированные изображения фракталов внутри неё.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
<?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
Фраза из английской похоронной службы, часто используемая для обозначения фатальной неизбежности конца.
Рассмотрим следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
<?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°
- обдуваемое ветром обнажённое дерево Пифагора — сочетаются первые два пункта
Мы же рассмотрим дерево Пифагора в классическом варианте. Вот его код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
<?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, впервые исследовавшим этот фрактал. К сожалению имя третьего физика — Брюса Бэнкса обычно опускают и вместо него ставят прочерк (некоторые люди к сожалению особенно подвержены действию закона Мёрфи).
Итак, рассмотрим следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
<?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. На этом тема фракталов и векторной графики не заканчивается. И мы более подробно раскроем её в следующей статье, которая будет посвящена фрактальной геометрии природы и грамматикам Линденмайера.