Mandelbrot в SVG
Большинство людей, в жилах которых течет кровь ботаников, видели множество Mandelbrot. Это загадочно, красиво и глубоко.
Уравнение также очень легко понять.
f(Z) = Z² + C
Для фрактала Мандельброта Z
изначально равно 0
, а C
— координата пикселя на SVG.
Несколько замечаний.
Z
иC
— комплексные числа.- SVG представляет собой сложную плоскость, где ось
x
представляет собой действительные числа, а осьy
— мнимые.
Я буду использовать написанный мной класс для выполнения арифметических операций над комплексными числами.
Поколение
Число считается включенным в набор, если при повторении бесконечное число раз величина значения остается небольшой. Для большинства начальных значений величина будет становиться все больше и больше вечно, это называется расхождением, они исключены из набора.
Чтобы вычислить значения, я создал две функции, одна из которых просто соответствует приведенному выше уравнению, а другая специально для Мандельброта.
/***
* <code>f(z) = z^2 + c</code>
* @param z Complex result of the iteration.
* @param c Complex constant, set to the coordinates on the complex plain of the point we are interested in.
***/
function f(z, c) {
(function assertArgs(z, c) {
if (!(z instanceof Complex))
throw new Error(
`Expected z to be Complex but was ${z?.constructor.name}`
);
if (!(c instanceof Complex))
throw new Error(
`Expected c to be Complex but was ${z?.constructor.name}`
);
})(z, c);
return z.multiply(z).add(c);
}
Для фрактала Мандельброта Z
изначально равно 0
, а C
- переменная, поэтому я создал другую функцию для приема C
, а также для принятия ряда итераций и возврата списка результатов.
/***
* Call <code>f(z,c)</code> for multiple iterations, returning an array of all the results.
* @param c
* @param maxIterations
* @returns {Complex[]}
*/
function mandelbrot(c, maxIterations) {
const results = new Array(maxIterations);
results[0] = f(Complex.zero(), c);
for (let i = 1; i < maxIterations; i++) {
const prev = results[i - 1];
results[i] = f(prev, c);
}
return results;
}
Отображение в SVG
Создаем SVG:
<svg id="mandelbrot-fractal"
width="500px"
height="500px"
viewBox="-1.5 -1 2 2">
</svg>
Для SVG мы определяем размер пикселя, затем перебираем каждый пиксель и вычисляем результаты. Размер пикселя определяется в константе PIXEL_SIZE
.
Если величина конечной точки превышает значение (о котором я только что догадался), то мы игнорируем его, если нет, я генерирую rect
в этой точке и определенного размера пикселя.
import { NS, drawXYAxisWithRings } from '../../../helpers/svg.js';
import { Complex } from '../../../helpers/Complex.js';
import { mandelbrot } from '../../fractals.js';
const PIXEL_SIZE = 0.0025;
const SVG = document.getElementById('mandelbrot-fractal');
const VIEW_BOX = {
x: SVG.viewBox.baseVal.x,
y: SVG.viewBox.baseVal.y,
width: SVG.viewBox.baseVal.width,
height: SVG.viewBox.baseVal.height,
};
SVG.appendChild(
drawXYAxisWithRings(VIEW_BOX.x, VIEW_BOX.y, VIEW_BOX.width, VIEW_BOX.height)
);
const END_X_COORD = VIEW_BOX.x + VIEW_BOX.width;
const END_Y_COORD = VIEW_BOX.y + VIEW_BOX.height;
const maxIterations = 100;
const maxMagnitude = 10;
const PIXEL_SIZE_STRING = PIXEL_SIZE.toString();
for (let i = VIEW_BOX.x; i < END_X_COORD; i += PIXEL_SIZE) {
for (let j = VIEW_BOX.y; j < END_Y_COORD; j += PIXEL_SIZE) {
const results = mandelbrot(new Complex(i, j), maxIterations);
if (results[results.length - 1].magnitude() < maxMagnitude) {
const pixel = document.createElementNS(NS, 'rect');
pixel.setAttribute('x', i.toString());
pixel.setAttribute('y', j.toString());
pixel.setAttribute('width', PIXEL_SIZE_STRING);
pixel.setAttribute('height', PIXEL_SIZE_STRING);
pixel.setAttribute('fill', 'black');
SVG.appendChild(pixel);
}
}
}
Это создает статическое полное изображение набора, похожее на изображение заголовка этой статьи.
Вывод
Честно говоря, я не думал, что это будет так просто. Я ожидал, что мне придется потратить много времени, пытаясь точно настроить то, как я определял, что на самом деле означало расхождение. Было удивительно, что мне сошло с рук что-то столь простое, как тестирование последнего результата на удивительно низкое значение.
Мы расширяем возможности SVG в количестве генерируемых узлов, однако при уменьшении размера пикселя быстро генерируется больше узлов, чем может обработать страница, что приводит либо к замедлению, либо просто к сбою страницы. Это мешает нам создавать что—либо похожее на красивые изображения, которые мы часто видим в Интернете (см. Набор Мандельброта - Википедия или выполните поиск по масштабированию Мандельброта).
Я подумывал добавить навигацию, так как было бы неплохо иметь панорамирование и масштабирование, однако постоянное удаление и перегенерация узлов поверх пересчета функции mandelbrot
сделало бы его очень неприятным в использовании, и я думаю, что изрядная работа для неудовлетворительный результат. Навигация и масштабирование возможны путем ручного изменения значений viewBox
SVG и константы PIXEL_SIZE
.