Нотатки програмістів: статті з програмування

Фрактали Жюліа на JavaScript.

Основи

Термін «фрактал» не має єдиного загальноприйнятого чіткого визначення. Як правило, фракталами називають фігуру, що має властивості самоподібності чи нетривіальності, або має дробову розмірність. Існує велика кількість таких фігур як штучного (сніжинка Коха, серветка Серпінського), так і природного (рослини, істоти) походження. З іншого боку, фракталами можуть бути алгебраїчні множини, що мають ці ж властивості. Типовими їх представниками є множини Мандельброта чи Жюліа, про які ми хочемо поговорити більш детально.

Так, вам не почулося, фрактали дійсно мають дробову розмірність. Це означає, що вони ані двомірні, ані тривимірні, а, скажімо, 2.5-мірні чи 1.31-мірні. Таке важко уявити, принаймні, поки як слід не поринути у фрактальну математику. З часом починаєш дивуватись іншому - як це так трапилось, що наш світ є рівно тривимірним, не близько до трійки, а рівно три і це при тому, що багато природних об'єктів мають фрактальну структуру. Але це згодом, поки що основи. Простіше сказати, що фрактали є самоподібними. Тобто кожна його частина при збільшенні схожа на увесь фрактал уцілому. І так до нескінченності - скільки завгодно фрактал можна збільшувати, він ніколи не перетвориться на суцільну фігуру.

Зосередимось на алгебраїчних фракталах, що є образами певних математичних множин. Математично задається правило, як побудувати цю множину, а якщо її уявити як точки деякого холста, то ці точки утворять певний рисунок, який і буде алгебраїчним фракталом. Власне так, частіше за все, фрактали і будують: беруть за основу певне правило і далі точка за точкою формують множину і її зображення. Французький математик Гастон Жюліа запропонував наступне правило побудови таких множин:
- координати точки на холсті (х та у) перетворюються у комплексне число z = x + iy
- для кожної з точок виконується перетворення z = z2 + C, де С - комплексна константа, однакова для всіх точок
- після великої кількості перетворень число z або починає необмежено зростати (розбігатись), або циклічно змінюватись в обмежених рамках (збігатись). Якщо у даній точці спостерігається розбіжність, то таку точку ставлять білим кольором, якщо збіжність - то чорним.
- процес повторюють для всіх точок холста, унаслідок чого утворюється певний образ.
У якості прикладу, натисність на порожній квадрат у цьому параграфі: це запустить побудову на ньому фракталу Жюліа з С = (0.315, 0.025).

Якщо казати більш широко, то перетворення для побудови фракталів не обмежується наведеним вище виразом z = z2 + C. Насправді, фрактал можна побудувати практично з довільного перетворення z = F(z) і різні види функцій F(z) будуть створювати різні форми для образів фракталів. Звичайно, є такі функції, що для жодної з її точок не буде чорного кольору, але такі функції нас не цікавлять. Частіше за все, функції F(z) є поліномами, але формальних обмежень немає. На цьому рисунку використана функція п'ятого ступеня F(z) = z5 + C з С = (0.61448, 0.53110). Натисність на рамку рисунка для старту його побудови. Можливо, на це знадобиться деякий час, зачекайте завершення рисування.

Реалізація

Розглянемо, як практично створюються рисунки фракталів. По-перше, слід зауважити, що основне фрактальне перетворення z = z2 + C відбувається у комплексних числах. Не вдаючись у глибини математики, відзначимо, що комплексне число - це насправді два звичайні числа (дійсна та уявна частини), а операція множення комплексних чисел (або піднесення до квадрату) зовсім не схожа на множення звичайних чисел. Якщо відмовитись від використання спеціалізованих бібліотек, то алгоритм перетворення буде виглядати наступним чином. Вводяться по дві змінні для кожного комплексного числа (z та С): var reC,imC,reZ,imZ; Далі їм призначаються значення (про це трохи згодом), і запускається цикл перетворення. Один крок (ітерація) такого циклу має вигляд: let x = reZ * reZ - imZ * imZ + reC;
let y = 2 * reZ * imZ + imC;
reZ = x;
imZ = y;
Саме так виглядають операції множення і додавання комплексних чисел.

Далі нам треба дослідити збіжність перетворення. Тобто повторювати крок за кроком наведений вище код і спостерігати за поведінкою z (reZ,imZ). З точки зору програмування, збіжність послідовності неможна досліджувати нескінченно. На це просто не вистачить часу - нескінченна кількість перевірок потребує нескінченно тривалого процесу. А з точки зору математики, точну збіжність (математичну рівність чисел) можна досягти лише на нескінченності. На будь-якому проміжному кроці числа рівні лише приблизно, з певним відхиленням одне від іншого. На практиці доведеться обмежитись деякою кількістю кроків перетворення. Чим більша ця кількість, тим довше буде рисуватись фрактал, але у той же час, тим більш детальним він вийде. Як правило, кількість кроків обирають від 100 до 1000 в залежності від бажаного ефекту. Ми встановимо обмеження на рівні 255 повторень. Чому обране саме таке число, буде зрозуміло далі.

Якщо послідовність є розбіжною, то ця розбіжність може проявитись значно раніше, ніж за 255 кроків. Відповідно, цикл повторення повинен мати дві умови: або здійснено граничну кількість кроків, або вже можна засвідчити розбіжність послідовності. З точки зору математики, послідовність стає розбіжною, якщо модуль числа z починає перевищувати корень з двох. Рахувати корені - це досить працеємна справа, тому візьмемо більш просту, хоч і менш точну умову: сума модулів reZ та imZ перевищує трійку. Можливо, менш точна умова призведе до того, що буде виконаний зайвий крок циклу, але це все одно швидше, ніж обчислювати корені на кожній ітерації. Отже, код набуває наступного вигляду: var n = 0;
while(n < 255 && Math.abs(reZ) + Math.abs(imZ) < 3) {
n++;
let x = reZ * reZ - imZ * imZ + reC;
let y = 2 * reZ * imZ + imC;
reZ = x;
imZ = y;
}
Оскільки цикл має дві умови завершення, після виходу з циклу слід додатково перевірити, яка з умов спрацювала. Якщо змінна n досягла 255, то вважаємо перетворення збіжним і ставимо чорну точку, інакше - білу.

Далі наша задача просканувати усі точки рисунка і для кожної з цих точок перевірити збіжність перетворення. Отже, додаємо на сторінку елемент <canvas id="fig">. Далі визначаємо його ширину та висоту і починаємо сканувати: let W = fig.width;
let H = fig.height;
let dc = fig.getContext("2d");
dc.clearRect(0,0,W,H);
dc.beginPath();
for(let j=0; j for(let i=0; i reZ = (-1.2+i*2.4/W);
imZ = (-1.2+j*2.4/H);
n=0;
while(n<255 && ...
if(n<255){ dc.fillStyle = "black"; }
else{ dc.fillStyle = "white"; }
dc.fillRect(i,j,1,1);
}
dc.closePath();dc.stroke();
Цикли по i та j сканують холст за шириною (W) та висотою (H). У кожній точці визначається reZ = (-1.2+i*2.4/W); за принципом масштабування: в межах рисунка reZ змінюється від -1.2 до +1.2. Саме у цих межах можна очікувати збіжність фрактального перетворення. Це знов таки приблизні значення, математика дає більш точні числа, але у нашому випадку цього цілком досить. Аналогічно виконується обробка imZ. Цикл у коді наведено скорочено, оскільки вище вже була наведена повна версія.

Додамо трохи кольору та придивимось поближче

Монохромні фрактали вже є досить цікавими, але у кольорі вони виглядають ще краще. Якщо казати більш-менш точно, то колір у фракталах відповідає не за самі точки фракталу, а за точки, які йому не належать, але є дуже близькими до фракталу. Іншими словами, фрактальне перетворення є розбіжним, проте, розбіжність починається не відразу, а через досить суттєву кількість зроблених кроків. Ми обмежили граничну кількість кроків величиною 255 якраз для того, щоб зручніше було встановлювати кольори прикордонних точок. Якщо казати про відтінки сірого, то саме число n після завершення циклу буде відповідати за яскравість точки. Точніше, за інверсну яскравість: якщо n=255, то точка має бути повністю чорна, а якщо n=0, то повністю біла. Інші значення можна перетворювати у відтінки сірого і наносити як кольорову інформацію на рисунок.

Але сірий колір створює ефект плавного переходу і тим самим "змиває" красиві границі фракталу. Бажано навпаки, створити ефект суттєвої зміни кольору в залежності від змін числа n. Для побудови фракталу з попереднього блоку було вжито наступний алгоритм для формування кольору: var r, g, b;
if(n<20){ r = g = b = 255-n; }
else if(n < 100) { r = 255-n+20; g = b = 255-n-5; }
else if(n < 200) { g = 255-n+20; r = b = 255-n-5; }
else { b = 255-n-3; g = r = 255; }
dc.fillStyle = "rgb("+r+","+g+","+b+")";
dc.fillRect(i,j,1,1);
Сутність алгоритму у тому, що для n у діапазоні до 100 виділяється червоний компонент (r), від 100 до 200 - зелений (g), інакше - блакитний (b). Це покращує контраст між точками і надає кольорової варіантності.

І, нарешті, кілька слів про практичний спосіб дослідження самоподібності фракталів. Задля цього їх треба збільшити і подивитись на якусь частину. Але якщо просто збільшити рисунок, то ми не досягнемо бажаного ефекту, оскільки інформацію про точки між точками неможна одержати з наявних на рисунку точок. Необхідно знову проводити перерахунок і перевіряти збіжність перетворення у кожній з точок, які нас цікавлять. Простіше за все змінити правило масштабування рисунку. Замість наведеного вище правила створимо, для прикладу, наступне: reZ = (-0.05+i*0.3/W);
imZ = (-0.1+j*0.3/H);
Це означатиме, що область фракталу замість діапазону (-1.2; 1.2) перетворюється в область з розміром 0.3, при цьому рисунок займає всю область холста, тобто кожна з його точок містить справжню фрактальну інформацію. Додатково у правилах зроблене різне зміщення, по горизонталі на величину -0.05, а по вертикалі на -0.1. Це дозволяє вибрати довільну частину фракталу для зображення та дослідження. Натисність на ближній рисунок - буде побудовано збільшений фрагмент фракталу.

Повний код прикладу

Даний код можна скопіювати у новий HTML-файл та відкрити у браузері. Маєте побачити приблизно те ж саме, що й на рисунку у даному розділі (натисність на нього для запуску розрахунку фракталу) <canvas id="fig" width="200" height="200" ></canvas>
<script>
var reC=0.315,
imC=0.027,
W=fig.width,
H=fig.height,
reZ,imZ,n,x,y;
let dc=fig.getContext("2d");
dc.clearRect(0,0,W,H);
dc.beginPath();
for(let j=0;j for(let i=0;i reZ = (-1.2+i*2.4/W);
imZ = (-1.2+j*2.4/H);
n=0;
while(n<255 && Math.abs(reZ)+ Math.abs(imZ) < 3) {
x = reZ*reZ-imZ*imZ+reC;
y = 2*reZ*imZ+imC;
reZ = x; imZ = y; n++;
}
var r,g,b;
if(n<20){r=g=b=255-n;}
else if(n<100){r=255-n+20;g=b=255-n-5;}
else if(n<200){g=255-n+20;r=b=255-n-5;}
else{b=255-n-3;g=r=255;}
dc.fillStyle = "rgb("+r+","+g+","+b+")";
dc.fillRect(i,j,1,1);
}
dc.closePath();dc.stroke();
</script>

Залишити коментар

Ім'я (відображається)
E-mail (не відображається)