Pull to refresh

Осваиваем F#: построение красочного множества Мандельброта с навигацией и интеграция с C#

Reading time 7 min
Views 13K

Вступительное слово


Данная статья расчитана на тех, кто уже хотя бы немного знаком с языками С# и F#. В любом случае, я старался сделать код как можно более читабельным и давать описание каждому фрагменту. Ознакомиться с языком F# можно в следующих статьях:

На Хабре уже много писали общих слов о языке F# — его истории, происхождении, особенностях. Не хочется повторяться, поэтому предлагаю сразу перейти к делу. Итак, план действий следующий:
  1. Построение множества Мандельброта;
  2. Визуализация результатов;
  3. Интеграция с C# и не только с ним.
Итак, поехали.


Построение множества Мандельброта


Пример построения множества Мандельброта на языке F# уже рассматривался на Хабре, мы же будем придерживаться немного другого подхода и рассмотрим некоторые моменты более детально.

Как определяется множество Мандельброта?

Рассмотрим последовательность чисел, генерируемых следующим уравнением:
Cn+1 = Cn2 + c0
Если такая последовательность не выходит за рамки двух комплексных чисел C1(1, 1i) и C2(-1, -1i), то комплексное число c0 принадлежит множеству Мандельброта. Значит, множество Мандельброта — это множество всех таких чисел c0, при котором рассмотренная выше последовательность остается в рамках C1 и C2.

Множество Мандельброта и F#

Для работы с комплексными числами в F# существует библиотека Microsoft.FSharp.Math, которая, в свою очередь, доступна в пакете FsharpPowerPack (по ссылке также можно найти руководство по установке и другую полезную информацию о данном паке). Добавляем данную библиотеку в наш проект и указываем ее:

open Microsoft.FSharp.Math

Теперь мы можем в программе манипулировать комплексными числами. Определим минимальную и максимальную границы:

let cMax = complex 1.0 1.0
let cMin = complex -1.0 -1.0

Перейдем непосредственно к функции. Реализуем функцию проверки на принадлежность к множеству следующим образом:

let rec isInMandelbrotSet (z, c, iter, count) =
    if (cMin < z) && (z < cMax) && (count < iter) then
        isInMandelbrotSet ( ((z * z) + c), c, iter, (count + 1) )
    else count

Рекурсивная функция isInMandelbrotSet работает, пока проверяемое число не вышло за границы cMax и cMin, а глубина рекурсии не превысила числа итераций. По завершении, функция возвращает число совершённых шагов рекурсии (это пригодится нам в дальнейшем, когда мы будем «раскрашивать» наше множество).

Визуализация результатов


Само множество мы уже построили, осталось лишь его вывести на экран. Но тут и начинается самое интересное.

Так как комплексные числа состоят из двух частей, то мы можем создать их отображение на двумерной плоскости. Множество Мандельброта существует между C1(1, 1i) и C2(-1, -1i), поэтому нужная нам система координат будет иметь центр в точке (0, 0), оси абсцисс и ординат будут ограничены значениями -1.0 и 1.0.
image

Поэтому нам необходимо осуществить перенос координат точек с комплексной плоскости на ту, которая используется при построении изображения.

Сделаем это следующим образом:

let scalingFactor s = s * 1.0 / 200.0

let mapPlane (x, y, s, mx, my) =
    let fx = ((float x) * scalingFactor s) + mx
    let fy = ((float y) * scalingFactor s) + my
    complex fx fy

Данная функция будет возвращать для каждой точки «привычной» плоскости соответствующее ей комплексное число. Величины mx и my мы будем использовать в дальнейшем для реализации навигации по нашему изображению.

Теперь мы можем легко отрисовать все наше множество на привычной плоскости. Для этого нам необходимо «пройтись» по всем координатам плоскости и проверить принадлежность каждой точки (точнее, соответствующего ей комплексного числа) к множеству Мандельброта. Если точка принадлежит множеству, сделаем её черной, в противном случае — раскрасим ее в другой цвет. Для этого, сделаем специальную функцию «раскраски»:

let colorize c =
    let r = (4 * c) % 255
    let g = (6 * c) % 255
    let b = (8 * c) % 255
    Color.FromArgb(r,g,b)

Функция будет принимать число итераций, которое мы получили из функции isInMandelbrotSet, и определять RGB-значение цвета. Числовые коэффициенты можно поставить любые, но чтобы добиться плавности перехода цветов, желательно задавать их с маленькой разницей. Также, здесь нам потребуется библиотека System.Drawing:

open System.Drawing

При помощи данной библиотеки, мы создадим новое bitmap-изображение и будем добавлять к нему точки определнного цвета. Так, наша функция отрисовки будет иметь следующий вид:

let createImage (s, mx, my, iter) =
    let image = new Bitmap(400, 400)
    for x = 0 to image.Width - 1 do
        for y = 0 to image.Height - 1 do
            let count = isInMandelbrotSet( Complex.Zero, (mapPlane (x, y, s, mx, my)), iter, 0)
            if count = iter then
                image.SetPixel(x,y, Color.Black)
            else
                image.SetPixel(x,y, colorize( count ) )
    let temp = new Form() in
    temp.Paint.Add(fun e -> e.Graphics.DrawImage(image, 0, 0))
    temp

Здесь мы используем компонент System.Windows.Forms:

open System.Windows.Forms

Осталось только запустить нашу программу и порадоваться результатом. Сделать это можно следующим образом:

do Application.Run(createImage (1.5, -1.5, -1.5, 20))

Мы указываем начальные параметры: масштаб, смещение по X и Y, а также число итераций. Чем число итераций больше, тем детальнее будет наше изображение (и, соответственно, дольше будет создаваться). Примерно таким должен получиться результат работы:
image

Итак, полный листинг нашей программы:

#light
open Microsoft.FSharp.Math
open System
open System.Drawing
open System.Windows.Forms

let cMax = complex 1.0 1.0
let cMin = complex -1.0 -1.0

let rec isInMandelbrotSet (z, c, iter, count) =
    if (cMin < z) && (z < cMax) && (count < iter) then
        isInMandelbrotSet ( ((z * z) + c), c, iter, (count + 1) )
    else count

let scalingFactor s = s * 1.0 / 200.0
let offsetX = -1.0
let offsetY = -1.0

let mapPlane (x, y, s, mx, my) =
    let fx = ((float x) * scalingFactor s) + mx
    let fy = ((float y) * scalingFactor s) + my
    complex fx fy

let colorize c =
    let r = (4 * c) % 255
    let g = (6 * c) % 255
    let b = (8 * c) % 255
    Color.FromArgb(r,g,b)

let createImage (s, mx, my, iter) =
    let image = new Bitmap(400, 400)
    for x = 0 to image.Width - 1 do
        for y = 0 to image.Height - 1 do
            let count = isInMandelbrotSet( Complex.Zero, (mapPlane (x, y, s, mx, my)), iter, 0)
            if count = iter then
                image.SetPixel(x,y, Color.Black)
            else
                image.SetPixel(x,y, colorize( count ) )
    let temp = new Form() in
    temp.Paint.Add(fun e -> e.Graphics.DrawImage(image, 0, 0))
    temp

do Application.Run(createImage (1.5, -1.5, -1.5, 20))


Промежуточные итоги

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

Конечно, можно создать все элементы интерфейса внутри F# так же, как мы создавали саму форму, используя библиотеку System.Windows.Forms. Но с другой стороны, было бы куда интереснее (и, пожалуй, логичнее) сделать это в рамках полноценного Windows Forms приложения! Хорошо, тогда этим сейчас и займемся.

Интеграция с C# и не только с ним


После сборки F#-приложения, на выходе мы получаем библиотеку, содержащую весь необходимый нам функционал. Для удобного доступа к необходимым функциям этой библиотеки, объявим namespace в коде F#. Делается это следующим образом: в самом начале кода добавим

module Fractal

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

let createImage (s, mx, my, iter) =
    let image = new Bitmap(400, 400)
    for x = 0 to image.Width - 1 do
        for y = 0 to image.Height - 1 do
            let count = isInMandelbrotSet( Complex.Zero, (mapPlane (x, y, s, mx, my)), iter, 0)
            if count = iter then
                image.SetPixel(x,y, Color.Black)
            else
                image.SetPixel(x,y, colorize( count ) )
    image

Вызываемая функция createImage возвращает созданное bitmap-изображение, содержащее множество Мандельброта.

Используем F#-библиотеку в Windows Forms

На самом деле, все очень просто: необходимо лишь добавить готовую библиотеку в новый проект (наример, Windows Forms или любой другой). Создав проект, в MS Visual Studio сделать это можно командой Add Reference окна Solution Explorer, выбрав необходимую библиотеку. Так как ранее мы обозначили namespace в библиотеке (module Fractal), то в новом проекте нам доступны все функции этой библиотеки. Теперь вызвать функцию, которая сгенерирует готовое изображение, можно следующим образом:

Bitmap image = Fractal.createImage(1.5, -1.5, -1.5, 20);

Полученное bitmap-изображение можно уже использовать любым способом — например, добавить в качестве background'а к элементу PictureBox.

Добавление элементов навигации

Как вы видите, мы можем «запросить» генерацию изображения с различными параметрами: масштабом, количеством итераций, смещением относительно X и Y. А значит, добавить навигацию не составит труда. В качестве контейнера для нашего изображения возьмем PictureBox, навигацию осуществим при помощи 6-ти кнопок: приблизить/отдалить, сдвинуть вверх/вниз, вправо/влево.

Для удобства создадим отдельный класс FractalClass, который будет хранить состояние текущего фрактала: масштаб, смещения от центра, уровень приближения. Основной метод класса будет запрашивать изображение множества с текущими параметрами.

        private Bitmap Draw()
        {
            int iters = iterations + 2 * steps;
            return Fractal.createImage(currSc, currMvX, currMvY, iters);
        }

Остальные методы класса будут изменять состояние фрактала и обращаться к методу Draw(). Так, например, может выглядеть метод приближения:

        public Bitmap ZoomIn()
        {
            scale = 0.9 * scale;
            zoomSteps++;
            return Draw();
        }

Осталось только добавить в обработку нажатия клавиш навигации вызов соответсвующих методов класса FractalClass. Результат будет примерно следующим:
image

Возможность передавать в функцию нужное количество итераций позволяет детализировать фрактал при приближении. Поэтому с каждым шагом изображение становится всё интереснее и интереснее:
image

Не только Windows Forms

Точно так же, как мы применили функции подключенной библиотеки в приложении Windows Forms, мы можем использовать готовую библиотеку в любом другом приложении платформы .NET: будь то Silverlight, веб-приложение или что-либо другое.

Используемые материалы:

Спасибо всем за внимание, надеюсь, было интересно!

P.S.: просьба об ошибках/неточностях в тексте уведомлять личными сообщениями.
Tags:
Hubs:
+33
Comments 12
Comments Comments 12

Articles