Работая над программой “Лист папоротника” и почитывая понемногу “Язык программирования 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))
];
А вот вам поистине прикольное зрелище, но без описания (его найдете в коллекции):
