Работая над программой «Лист папоротника» и почитывая понемногу «Язык программирования D», я подумал над тем, что программа построения этой системы итерированных функций могла бы дать гораздо большую свободу для экспериментов и творчества в области фракталов. Собственно, задумался я о возможностях той простенькой программы еще в момент ее написания, именно в этот момент я видел главный ее недостаток (на мой взгляд, конечно) — программа слишком специфична, так как выводит лишь одну единственную, хотя и красивую IFS, а кроме того, для получения других красивых систем приходится ее переписывать практически с нуля…
Мое стремление к универсальности, к обобщенности всего и вся и победило в этом случае, кроме того, пришлось немного подумать, чтобы решить необычную задачу: сделать программу универсальной, но так, чтобы можно было в ней легко описать любую систему итерированных функций, которая может взбрести в голову, но обо всем по порядку.
Для начала создаем новый пустой проект DGui, используя в качестве шаблона предыдущую программу, в которой нас интересует только код генерации формы и перегруженный метод onPaint.
Это только начало, но немного с другой стороны.
Во-первых, откуда взять новые IFS и как сделать их описание удобным практически для всех возможных случаев ? Как ни странно, этот вопрос легко решить: необходимо лишь немного поискать в интернете системы итерируемых функций, и в итоге можно наткнуться на целую коллекцию интересных уравнений, дополненных их изображениями.
Используя коллекцию, не так уж трудно будет нарисовать сам рисунок, не правда ли ?
Однако, та коллекция на которую я по случайности наткнулся, может помочь не только тем, что в ней содержится, но еще и тем, что там прямо в начале содержится подсказка как сделать отрисовку фракталов и прочих изысков сказочно универсальной.
И эта подсказка выглядит так:
Fractal { a1 b1 c1 d1 e1 f1 p1 a2 b2 c2 d2 e2 f2 p2 .............. an bn cn dn en fn pn }
Что это такое ?
Это описание формата одной из программ для построения фракталов и оно на редкость удобно: Fractal, в данном отрезке кода, это имя сущности, предназначенной для построения; а дальше еще проще — каждая строка соответствует одному уравнению преобразования (an, bn, cn, dn, en, fn — это коэффициенты уравнения; pn — вероятность, с которой уравнение будет применено к некоторой точке рисунка).
Как видно, за нас проделали большую часть работы, более того, на страницы под обрывком кода приведено объяснение того, как это интерпретирует программа !
Легко заметить, что работа программы сводится к тому, чтобы в цикле рассчитывать новые точки, исходя из этих уравнений, при этом вся математика сводится к простым операциям над матрицами: умножение и сложение.
Итак, дано: некоторые точки экранной плоскости и набор преобразований, применяемых в зависимости от того, как распределяется некоторая случайная величина.
Достаточно обобщенно, не так ли ?
Для начала пересмотрим в корне работу программы: создадим некоторую абстракцию, которая будет представлять собой точку плоскости и ряд полезных инструкций по работе с ней:
// точка struct Point2D { float x, y; void drawPoint(Canvas c) { Pen p = new Pen(SystemColors.darkGreen, 1, PenStyle.solid); auto X = cast(int) (250 - 200 * x); auto Y = cast(int) (400 - 200 * y); c.drawLine(p, X, Y, X + 1, Y + 1); } };
Как видно, это простейшая структура, которая принимает координаты (x; y) и содержит в себе очень важный метод, который позволит поставить точку на экране, ничего необычного тут нет.
Сами преобразования над точками плоскости не представляют собой особой проблемы: для удобного описания уравнений, лежащих в основе IFS, мы будем использовать структуру Equation, которая выглядит так:
// уравнение, описывающее IFS struct Equation { float[6] coeff; // коэффициенты Point2D p; // точка для преобразований @property Point2D calc() { auto mul0 = coeff[0] * p.x + coeff[1] * p.y; auto mul1 = coeff[2] * p.x + coeff[3] * p.y; return Point2D(mul0 + coeff[4], mul1 + coeff[5]); } };
Структура принимает массив коэффициентов уравнения, а также некоторую точку плоскости, которая поначалу будет использована как удобная заглушка в свойстве calc, реализующем всю математику с матрицами. Свойство calc, как будет показано далее, нам очень и очень пригодится.
Точки и преобразования описаны, однако, сам процесс отрисовки не описан. Процесс рисования IFS удобнее всего вынести в отдельную функцию, которая бы принимала в качестве аргументов некоторый «список» уравнений, «список» вероятностей применения этих самых уравнений, а также некоторую «стартовую точку» из которой начинается рост IFS, ну и самый важный аргумент в процессе рисования — это холст, на котором будет отображено наше художество.
Функция отрисовки выглядит так:
void drawIFS(Equation[] eqn, float[] pr, Point2D start, Canvas c) { for (int i = 0; i < 50_000; i++) { auto d = dice(pr); auto eq = eqn[d]; eq.p = start; start = eq.calc; start.drawPoint(c); } }
В ходе своей работы, drawIFS принимает предварительно созданный массив из структур Equation (по сути дела, это и есть вся система уравнений), предварительно созданный массив чисел, ответственных за вероятности, структуру Point2D, задающую начальную точку, а также объект Canvas.
Внутри функции есть цикл, в котором и происходит самое интересное: в переменную d попадает значение функции dice из std.random, которая выдает целые значения от 0 до некоторого числа (совпадающего с количеством переданных в нее аргументов) с вероятностями, которые были переданы в качестве входных параметров. Но поскольку, dice — универсальная функция, да и к тому же с переменным числом аргументов, то пользуясь фишкой языка D, связанной с определением таких функций, в нее можно передать вместо одиночных параметров, их массив, что и происходит: в функцию dice попадает массив, описывающий вероятности.
На этом интересности не кончаются, и переменная d используется как индекс массива с уравнениями, и таким образом, случайным образом (и в то же время, с весьма определенной вероятностью !) выделяется уравнение для преобразования. После этого финта ушами, для снятия уже упоминавшейся заглушки «пустой точки» в структуре Equation, в выделенное уравнение помещается точка start от предыдущей итерации (естественно, «предыдущей» итерацией для этой точки в самом первом проходе цикла служит значение аргумента start функции drawIFS). Далее вызывается свойство calc для того, чтобы просто произвести преобразование предыдущей точки в текущую, а затем используется серия нехитрых преобразований после которых свежеполученная точка выводится на экран.
После введения всех этих фрагментов кода в файл app.d не следуется расслабляться: необходимо внутри метода формы onPaint приготовить описания уравнений, описания вероятностей, а также вызвать функцию drawIFS для получения желанных результатов.
И вот тут я должен показать как это проделать.
Все это делается довольно просто — берем описание IFS из коллекции, допустим такое:
Fir_2 { 0.1000 0.0000 0.0000 0.1600 0.0 0.0 0.01 0.8500 0.0000 0.0000 0.8500 0.0 1.6 0.85 -0.1667 -0.2887 0.2887 -0.1667 0.0 1.6 0.07 -0.1667 0.2887 -0.2887 -0.1667 0.0 1.6 0.07 }
и приводим его в такой вид:
// елка // уравнения Equation[] eqn; eqn ~= [ Equation([-0.1000, 0.0000, 0.0000, 0.1600, 0.0000, 0.0000], Point2D(0.0, 0.0)), Equation([-0.8500, 0.0000, 0.0000, 0.8500, 0.0000, 1.6000], Point2D(0.0, 0.0)), Equation([-0.1667, -0.2887, 0.2887, -0.1667, 0.0000, 1.6000], Point2D(0.0, 0.0)), Equation([-0.1667, 0.2887, -0.2887, -0.1667, 0.0000, 1.6000], Point2D(0.0, 0.0)) ]; // вероятности float[] pr; pr ~= [ 0.01, 0.85, 0.07, 0.07 ];
в итоге получаем код приложения:
import dgui.all; import std.random; // точка struct Point2D { float x, y; void drawPoint(Canvas c) { Pen p = new Pen(SystemColors.darkGreen, 1, PenStyle.solid); auto X = cast(int) (250 - 200 * x); auto Y = cast(int) (400 - 200 * y); c.drawLine(p, X, Y, X + 1, Y + 1); } }; // уравнение, описывающее IFS struct Equation { float[6] coeff; // коэффициенты Point2D p; // точка для преобразований @property Point2D calc() { auto mul0 = coeff[0] * p.x + coeff[1] * p.y; auto mul1 = coeff[2] * p.x + coeff[3] * p.y; return Point2D(mul0 + coeff[4], mul1 + coeff[5]); } }; void drawIFS(Equation[] eqn, float[] pr, Point2D start, Canvas c) { for (int i = 0; i < 50_000; i++) { auto d = dice(pr); auto eq = eqn[d]; eq.p = start; start = eq.calc; start.drawPoint(c); } } class MainForm : Form { public this() { this.text = ""; this.size = Size(500, 600); this.startPosition = FormStartPosition.centerScreen; }; protected override void onPaint(PaintEventArgs e) { // елка Equation[] eqn; eqn ~= [ Equation([-0.1000, 0.0000, 0.0000, 0.1600, 0.0000, 0.0000], Point2D(0.0, 0.0)), Equation([-0.8500, 0.0000, 0.0000, 0.8500, 0.0000, 1.6000], Point2D(0.0, 0.0)), Equation([-0.1667, -0.2887, 0.2887, -0.1667, 0.0000, 1.6000], Point2D(0.0, 0.0)), Equation([-0.1667, 0.2887, -0.2887, -0.1667, 0.0000, 1.6000], Point2D(0.0, 0.0)) ]; // вероятности float[] pr; pr ~= [ 0.01, 0.85, 0.07, 0.07 ]; Canvas c = e.canvas; drawIFS(eqn, pr, Point2D(1.0, 1.0), c); super.onPaint(e); } }; int main(string[] args) { return Application.run(new MainForm()); }
что будет выглядеть примерно вот так:
Ну и напоследок:
Описание:
// лист клена Equation[] eqn; eqn ~= [ Equation([ 0.1400, 0.0100, 0.0000, 0.5100, -0.0800, -1.3100], Point2D(0.0, 0.0)), Equation([ 0.4300, 0.5200, -0.4500, 0.5000, 1.4900, -0.7500], Point2D(0.0, 0.0)), Equation([ 0.4500, -0.4900, 0.4700, 0.4700, -1.6200, -0.7400], Point2D(0.0, 0.0)), Equation([ 0.4900, 0.0000, 0.0000, 0.5100, 0.0200, 1.6200], Point2D(0.0, 0.0)) ]; // вероятности float[] pr; pr ~= [ 0.06, 0.37, 0.36, 0.21 ];
Описание:
// треугольник серпинского Equation[] eqn; eqn ~= [ Equation([-0.4000, 0.0000, 0.0000, -0.4000, 0.2400, 0.3700], Point2D(0.0, 0.0)), Equation([ 0.5000, 0.0000, 0.0000, 0.5000, -1.3700, 0.2500], Point2D(0.0, 0.0)), Equation([ 0.2100, 0.0000, 0.0000, 0.2100, 1.0000, 1.4700], Point2D(0.0, 0.0)), Equation([ 0.5000, 0.0000, 0.0000, 0.5000, 0.7600, -1.1600], Point2D(0.0, 0.0)) ]; // вероятности float[] pr; pr ~= [ 0.23, 0.36, 0.06, 0.36 ];
Описание:
// спираль Equation[] eqn; eqn ~= [ Equation([-0.8700, 0.2300, -0.2300, -0.8700, 0.0000, 0.0000], Point2D(0.0, 0.0)), Equation([-0.3400, 0.2100, -0.2100, -0.3400, 1.3400, 0.2100], Point2D(0.0, 0.0)) ];
А вот вам поистине прикольное зрелище, но без описания (его найдете в коллекции):