Усовершенствуем лист папоротника

Работая над программой «Лист папоротника» и почитывая понемногу «Язык программирования 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))
       ];

 

А вот вам поистине прикольное зрелище, но без описания (его найдете в коллекции):

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий