Jesień jest idealną porą na ogrodowe porządki. Przesadzanie i sadzenie. Będziemy sadzić, ale aby nie przesadzić. Nie przesadzić z rekurencją, bo ta będzie nam dziś towarzyszyć. Tym razem o fraktalu bezimiennym, choć w naturze często podpisywanym na korze inicjałami imion i serduszkiem. Bezimiennym, ale równie atrakcyjnym, a być może nawet bardziej niż ostatnio opisywany fraktal Pana Sierpińskiego.
Zróbmy dołek w wyjaśnieniach.
Nasze fraktalne drzewo będzie podobne do prawdziwego. Najpierw stworzymy idealne, potem dodamy trochę losowości, aby faktycznie przypominało to naturalne. Jedyna różnica będzie taka, że z każdej gałęzi będą wyrastać dwie i tylko dwie kolejne gałęzie. Czyli zgodnie z tytułem każda gałąź będzie mieć swoj początek i podwójny koniec.
Zaczniemy od pnia, który będzie wyrastać pod kątem 90 stopni względem podłoża. Znając jego koniec nakreślimy dwie gałęzie, które wyrosną odpowiednio pod kątem 90 + kąt pochylenia gałęzi oraz 90 - kąt pochylenia gałęzi. Z tych dwóch gałęzi wyrosną kolejne 4. Dla każdej pary przekażemy nowo przeliczony kąt i z niego wyrosną dwie kolejne gałęzie pod kątem obliczonym na identycznej zasadzie jak wcześniej (oczywiście zamiast 90 stopni będzie kąt pochylenia rodzica). Z tego końca wyrośną dwie kolejne gałęzie. I tak w kółko. Prawie, bo ograniczymy rozrost gałęzi do kilku poziomów. Już czuć powiew rekurencji i fraktalności, prawda?
Zasiejmy ziarno kodu.
<canvas></canvas>
+
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var level = 10;
var width = 400;
var height = 400;
canvas.width = width;
canvas.height = height;
/* długość pnia to 20% wysokości Canvas */
var branch = height - height * 0.8;
/* czyścimy Canvas */
ctx.clearRect(0, 0, width, height);
/* rozpoczynamy kreślenie */
ctx.beginPath();
draw(width / 2, height, branch, Math.PI / 2, level);
ctx.stroke();
function draw(x1, y1, branch, angle, level){
/* jeśli to nie ostatni poziom */
if(level > 0){
/* oblicz położenie końca na podstawie obrotu względem początku (x1,y1) i długości gałęzi (branch) */
var x2 = x1 + Math.cos(angle) * branch;
var y2 = y1 - Math.sin(angle) * branch;
/* narysuj gałąź */
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
/* kolejna gałąź będzie stanowić 80% długości poprzedniej */
branch *= 0.8;
/* wywołaj kreślenie dwóch gałęzi wyrastających z nowego końca pod katem 30 stopni */
draw(x2, y2, branch, angle + (Math.PI / 180 * 30), level - 1);
draw(x2, y2, branch, angle - (Math.PI / 180 * 30), level - 1);
}
}
Podlewajmy i podziwiajmy.
A gdybyś był wiatrem?
Skorzystajmy z tego, że możemy prowadzić interakcję ze stroną i w zależności od położenia Y myszy ustalmy wysokości pnia, a od położenia X kąt pochylenia gałęzi.
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var level = 10;
var branch;
var mouseX = mouseY = 0;
var width = 400;
var height = 400;
canvas.width = width;
canvas.height = height;
function draw(x1, y1, branch, angle, level){
if(level > 0){
var x2 = x1 + Math.cos(angle) * branch;
var y2 = y1 - Math.sin(angle) * branch;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
branch *= 0.8;
/* tu zamiast stałego kąta nachylenia gałęzi, dodajemy wartość położenia kursora */
draw(x2, y2, branch, angle + rescale(mouseX, 0, height, 0, Math.PI), level - 1);
draw(x2, y2, branch, angle - rescale(mouseX, 0, height, 0, Math.PI), level - 1);
}
}
/* funkcja rescale mapuje wartość z jednego przedziału do drugiego
w przykładzie używamy jej aby przekształcić położenie kursora myszy/dotyku
z przedziału <0,width> do kąta w przedziale <0,PI>
*/
function rescale(value, start1, stop1, start2, stop2){
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
}
/* na nasze drzewo będzie działać woda i wiatr w zależności od kursora */
document.addEventListener('mousemove', function(e){
/* pobieramy położenie Canvas i obliczamy pozycję kursora */
var canvasRect = canvas.getBoundingClientRect();
mouseX = e.clientX - (canvasRect.left + document.body.scrollLeft);
mouseY = e.clientY - (canvasRect.top + document.body.scrollTop);
/* długość gałęzi zależna od położenia Y */
branch = height - mouseY;
/* czyścimy Canvas */
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
draw(width / 2, height, branch, Math.PI / 2, level);
ctx.stroke();
});
(najedź myszą)
Drzewo bez oprysków.
Jak widzisz nasze drzewo jest trochę zbyt idealne. Piękne, ale nienaturalne. W naturze wszystkie gałęzie nie wyrastają pod tym samym kątem, każda pod innym. Wprowadźmy więc czynnik losowości do ostatniego przykładu. Zmieńmy tylko sposób kreślenia gałęzi:
draw(x2, y2, branch, angle + rescale(mouseX, 0, height, 0, Math.PI) + ((Math.PI / 180) * (Math.random()*70 - 35)), level - 1);
draw(x2, y2, branch, angle - rescale(mouseX, 0, height, 0, Math.PI) + ((Math.PI / 180) * (Math.random()*70 - 35)), level - 1);
Do każdego kąta gałęzi dodaliśmy losową wartość z przedziału <-35,35>. Przy czym wartość tak jest losowana przy każdorazowym kreśleniu drzewa (poruszeniu myszy). Jeśli efekt Cię drażni, przenieś wylosowane wartości na początek kodu. Wtedy nowe, losowe drzewo będzie generowane podczas odświeżania strony.
(najedź myszą)
"Lecz tylko Bóg może stworzyć drzewo." - Drzewa - Joyce Kilmer (1886-1918)
Przydatne linki:
Typowo męska logika, czyli fraktal Sierpińskiego