Вступительное слово
Данная статья расчитана на тех, кто уже хотя бы немного знаком с языками С# и F#. В любом случае, я старался сделать код как можно более читабельным и давать описание каждому фрагменту. Ознакомиться с языком F# можно в следующих статьях:
На Хабре уже много писали общих слов о языке F# — его истории, происхождении, особенностях. Не хочется повторяться, поэтому предлагаю сразу перейти к делу. Итак, план действий следующий:
- Построение множества Мандельброта;
- Визуализация результатов;
- Интеграция с 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.
Поэтому нам необходимо осуществить перенос координат точек с комплексной плоскости на ту, которая используется при построении изображения.
Сделаем это следующим образом:
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, а также число итераций. Чем число итераций больше, тем детальнее будет наше изображение (и, соответственно, дольше будет создаваться). Примерно таким должен получиться результат работы:
Итак, полный листинг нашей программы:
#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. Результат будет примерно следующим:
Возможность передавать в функцию нужное количество итераций позволяет детализировать фрактал при приближении. Поэтому с каждым шагом изображение становится всё интереснее и интереснее:
Не только Windows Forms
Точно так же, как мы применили функции подключенной библиотеки в приложении Windows Forms, мы можем использовать готовую библиотеку в любом другом приложении платформы .NET: будь то Silverlight, веб-приложение или что-либо другое.
Используемые материалы:
- Robert Pickering — Beginning F#
- Интеграция C# и F#
- Влюбляемся в F#: Доза 2: Строим фрактальное изображение
Спасибо всем за внимание, надеюсь, было интересно!
P.S.: просьба об ошибках/неточностях в тексте уведомлять личными сообщениями.