Mandelbrot kümesini bir çok insan duymuştur. Hani şeklin içine girdikçe büyük şeklin sonsuz tekrarıyla karşılaştığımız fraktal dizaynlar. Konunun matematiğine dalmayacağım, yani çok dalmayacağım. Sonuçta çoğu insan için bu şekilleri anlaşılır bir şekilde üretmenin yolunu javascript yardımıyla göstereceğim.
Öncelikle Julia kümesiyle başlayacağım. Mandelbrot kümesi başka bir yazının konusu olacak. Algoritmik olarak arada çok az bir fark olmasına rağmen matematiksel olarak farklı kümeler bunlar. Olay kompleks düzlemde geçmektedir. Hani kompleks sayılarla oluşturulan düzlem. Kompleks saylar deyince akla tabii ki meşhur \(i=\sqrt{-1}\) tanımı gelir.
Bu kompleks sayıların bir reel bir de sanal kısmı vardır. Yani verilen bir \(z \) kompleks sayısını \(z=a+ib \) şeklinde yazabiliriz. Burada \(a \) ve \(b \) sayıları bildiğimiz reel sayılardır. Aslında bu noktadan itibaren bu kompleks sayıları xy-koordinat sistemindeki noktalar gibi düşünebiliriz. \(a \) sayısı noktanın x koordinatını, \(b \) sayısı da noktanın y koordinatını göstersin.
Şimdi algoritmamızda verilen bir \(z \) kompleks sayısını (ya da düzlemdeki noktasını) nasıl dönüştüreceğimize bakalım.
\(f(z) = z^2 + c \)
Burada \(c \) sayısı da kompleks bir sayıdır. Bilmeyenler için kısaca yukarıdaki işlemi reel ve sanal kısımlar cinsinden yazacağım.
\(z = a + i b \) ve \(c = d + i e \) olsun.
\(z^2 = z \cdot z = {a \cdot a – b \cdot b} + i {2 \cdot a \cdot b} \)
\(z^2 + c= {a \cdot a – b \cdot b + d} + i {2 \cdot a \cdot b + e} \)
Şimdi hesap kısmını hallettiğimize göre yaptığımız dönüşümü indeksleyelim, yani her adımda bulduğumuz kompleks sayıya bir sıra numarası verelim.
\(z_{n+1} = z_n^2 + c \) (1)
Hesapladığımız toplamı bir sonraki adımda yeni kompleks sayı olarak kullanacağız. Her adımda bu kompleks sayının belli bir alanın dışına çıkıp çıkmadığına bakacağız. Bunun için de bu sayının orijine uzaklığını hesaplayacağız.
\(\lvert z \rvert = a \cdot a + b \cdot b \) (2)
Eğer bu nokta yarıçapı 2 olan dairenin dışındaysa sayının o adımdaki indeks numarasını alacağız ve başlangıçtaki noktamızı bu indekse göre ürettiğimiz bir renkle koordinat sistemimizde işaretleyeceğiz. Bu dönüşümler sırasında bazı başlangıç sayıları her zaman bu dairenin içinde kalabilir. Bu durumda programın devam etmesi için maksimum adım sayısı tanımlayacağız. En basit renklendirme olarak eğer maksimum adım sayısına ulaşılmışsa siyah, aksi durumda beyaz noktalar kullanılabilir.
Algoritma:
- \(c \) kompleks sayısı belirlenir. Bu sayı rastgele de seçilebilir ama her değer için güzel şekiller ortaya çıkmıyor.
- Çizim yapılacak ekranın her noktası için: Seçilen nokta bir \(z_0 \) kompleks sayısına dönüştürülür.
- Her adımda elde edilen kompleks sayı yarı çapı 2 olan dairenin dışına düşmediği ve maksimum adım sayısına erişilmediği sürece (1) numaralı dönüşüm uygulanır.
- Seçilmiş nokta için elde edilen adım sayısına göre bir renk üretilir.
- Seçilmiş nokta ekran üzerinde bu renge boyanır.
Örnek:
1. \(c=-0.5 + 0.5 i \) olsun.
2. Ekranımız 400 satır ve 400 sütundan oluşsun. Ekranın orta noktası düzlemimizin orijini olsun ve sol üst köşe koordinat sisteminde (-2, 2) noktası, sağ üst köşe (2, 2) noktası, sol alt köşe (-2, -2) noktası ve son olarak da sağ alt köşe (2, -2) noktası olsun. Şimdi ekran piksel konumlarını bu koordinatlara gönderen dönüşümü hazırlayalım.
Ekran koordinatlarına x ve y diyelim.
\(z = a + bi\longrightarrow{a=\frac{x – 200}{100},b=\frac{y – 200}{100}} \)
Burada ekran noktası olarak x = 225 ve y = 350 alalım. Bu değerleri yukarıdaki dönüşüme koyarsak
\(z_0 =\frac{225 – 200}{100}+\frac{350 – 200}{100}i=0.25+1.5i \) kompleks sayısını buluruz.
3. (1) numaralı dönüşümü uygulamaya başlayalım.
\(z_1=z_0^2-0.5+0.5i=(0.25 + 1.5i)\cdot (0.25+1.5i)-0.5+0.5i \)
\(z_1=0.625-2.25-0.5+(0.75+0.5)i=-2.125+1.25i \)
\(\lvert z_1 \rvert = \sqrt{2.125^2 + 1.25^2}=\sqrt{6.078125}=2.46 \)
demek ki seçtiğimiz nokta daha ilk adımda yarıçapı 2 olan dairenin dışına çıkıyor. Bu durumda adım sayısı 1 oluyor ve bu nokta için 1 indeksli rengi kullanıyoruz. İşin görsel kısmı tamamen bu renk seçimine bağlı. İlk denemelerde oldukça basit bir renk şeması seçtim. Eğer maksimum adım sayısına erişilmişse siyah, aksi durumda beyaz noktalar kullandım. Bu bile yeterli aslında ama sonra daha renkli şekiller oluşturmak için HSV renk uzayını denedim.
Aşağıdaki linkte yazdığım javascript uygulamasını deneyebilirsiniz.
yilmazaksoy.com/apps/Julia.html
Reel(c) ve Sanal(c) parametrelerine istenilen değerleri verdikten sonra Başlat düğmesine basarak şeklin oluşmasını bekleyebilirsiniz. Eğer sisteminizde şeklin ortaya çıkması çok uzun sürüyorsa adım sayısını azaltabilirsiniz. Bu durumda program daha hızlanacaktır ama çıkan sonuçta daha az renk kullanılacaktır.
Örneğin Reel(c) = -0.7 ve Sanal(c) 0.25 için aşağıdaki şekil elde edilebilir.
Program:
Kullanıcı girdileri ve çizimin yapılacağı alanı tanımlamak için aşağıdaki gibi bir blok kullandım. Basit bir uygulama olduğundan hiç CSS de kullanmadım.
[code language=”html”]
<div id=”first”>
Adım sayısı: <input id=”maxIterations” type=”number”step=”1″/>
</div>
<div id=”last”>Reel (c): <input id=”c_real” type=”number”step=”any”/>
Sanal (c):<input id=”c_imaginary” type=”number”step=”any”/>
<button id=”start” onclick=”startDrawing()”>Başlat</button>
</div>
<canvas id=”drawingArea” width=”400″ height=”400″ style=”border:1px solid #000000;”></canvas>
[/code]
Bu giriş elemanlarına ilk değerlerini atamak için sayfa yüklendiğinde init adlı fonksiyon çağrılıyor.
[code language=”javascript”]
<body onload=”init()”>
<script>
var c_real = 0.0;
var c_imaginary = 0.0;
var maxIterations = 300;
function init() {
document.getElementById(“maxIterations”).value = ‘300’;
document.getElementById(“c_real”).value = ‘0’;
document.getElementById(“c_imaginary”).value = ‘0’;
}
…
</script>
[/code]
Başlat düğmesine basıldığında startDrawing() adlı fonksiyon çağrılıyor:
[code language=”javascript”]
function startDrawing() {
c_real = parseFloat(document.getElementById(“c_real”).value);
c_imaginary = parseFloat(document.getElementById(“c_imaginary”).value);
maxIterations = parseInt(document.getElementById(“maxIterations”).value);
var canvas = document.getElementById(“drawingArea”);
var width = canvas.width;
var height = canvas.height;
var i = 0;
var j = 0;
for ( i = 0; i < width; ++i) {
for ( j = 0; j < height; ++j) {
var value = calculatePoint(i, j, width, height);
var numberOfIterations = checkPoint(value, [c_real, c_imaginary], maxIterations, 4);
plotPoint(i, j, numberOfIterations);
}
}
var img = canvas.toDataURL(“image/png”);
document.write(‘<img src=”‘ + img +'”/>’);
}
[/code]
İlk önce c_real, c_imaginary ve maxIterations değişkenleri kullanıcı girdilerinden okunuyor. İlk iki değer reel sayı, adım sayısı ise tam sayı olduğundan parseFloat ve parseInt fonksiyonları yardımıyla değerler doğru tiplere dönüştürülüyor. Ardından çizim alanımız olan canvas değişkeni okunuyor ve bu alanın satır ve sütun sayısı elde ediliyor. Sonra içiçe döngüyle bu çizim alanının bütün noktaları işleniyor. calculatePoint fonksiyonu ile bulunduğumuz satır ve sütundaki ekran noktası için bir kompleks sayı üretiyoruz. checkPoint() fonksiyonu ile bu kompleks sayının kaç adımda yarıçapı 2 olan dairenin dışına çıktığını kontrol ediyoruz. Son olarak da elde ettiğimiz adım sayısını kullanarak bu noktanın rengini belirliyoruz ve noktayı çiziyoruz.
[code language=”javascript”]
var img = canvas.toDataURL(“image/png”);
document.write(‘<img src=”‘ + img +'”/>’);
[/code]
Bu son iki satır ile canvas’ın içindeki şekli HTML dökümanının içine tekrar yazıyorum ki kullanıcı şekli isterse kaydedebilsin. Normalde masaüstü tarayıcıları bu satırlar olmadan da canvas’ın içeriğini kaydedebiliyor ama akıllı telefonlar ve tabletlerde bu her zaman mümkün değil anladığım kadarıyla.
[code language=”javascript”]
function calculatePoint(x, y, width, height) {
var real = (x – (width / 2)) * 4.0 / width;
var imaginary = (y – (height / 2)) * 4.0 / height;
return [real, imaginary];
}
[/code]
Bu fonksiyon ekran koordinatlarını x ve y koordinatları -2 ve 2 arasındaki reel sayılar olacak şekilde yeni noktalara çeviriyor.
[code language=”javascript”]
function checkPoint(z, c, maxIterations, radius) {
var i = 0;
var p = z;
for ( i = 0; i < maxIterations; ++i) {
p = calculateNewValue(p, c);
if (!isPointInCircle(p, radius)) {
return i;
}
}
return i;
}
[/code]
Bu fonksiyon elimizdeki kompleks sayının kaç adımda dairenin dışına çıktığını buluyor. Tabii ki adım sayısı maksimum adım sayısını geçerse aramayı kesiyoruz. Her adımda bir sonraki kompleks sayıyı üretiyoruz (calculateNewValue) ve noktayı kontrol ediyoruz (isPointInCircle).
[code language=”javascript”]
function calculateNewValue(z, c) {
var realPart = z[0] * z[0] – z[1] * z[1] + c[0];
var imaginaryPart = 2 * z[0] * z[1] + c[1];
return [realPart, imaginaryPart];
}
[/code]
Bu fonksiyon da \(z_{n+1} = z_n^2 + c\) dönüşümünü uyguluyor. Burada kompleks sayıları iki elemanlı javascript dizileri olarak tanımladım (z ve c değişkenleri) bu nedenle [] index operatörü ile reel ve sanal kısımlarına erişebiliyoruz ve aynı şekilde iki elemanlı bir javascript dizisi geri döndürüyoruz.
[code language=”javascript”]
function isPointInCircle(z, radius) {
var norm = z[0] * z[0] + z[1] * z[1];
if (norm < radius) {
return true;
}
return false;
}
[/code]
Bu fonksiyonda aslında normu yukarıda tanımladığım gibi kullanmadım. Karekökü almadım, yerine yarıçapı 2 değil de karesi olan 4 olarak kullandım. Bu şekilde oldukça yavaş olan karekök işlemini optimize etmiş olduk.
[code language=”javascript”]
function plotPoint(x, y, index) {
var c = document.getElementById(“drawingArea”);
var ctx = c.getContext(“2d”);
if (index < maxIterations) {
ctx.fillStyle = HSVtoRGB(index/Math.PI, 1.0, 1.0);
} else {
ctx.fillStyle = HSVtoRGB(index/Math.PI, 1.0, 0.0);
}
ctx.fillRect(x, y, 1, 1);
}
[/code]
Bu fonksiyon da adım sayısına göre noktayı rengiyle beraber çiziyor. Dikkat edilirse nokta yerine büyüklüğü 1 piksel olan dikdörtgen kullandım, javascript’te nokta çizme fonksiyonu yok. Bu istediğime yeterince yakın sonuçlar verdi ama. HSVtoRGB fonksiyonu da adım sayısından canvas’ta kullanılacak RGB renk kodunu üretiyor. Bu fonksiyonun kodunu aşağıdaki 2 numaralı kaynaktan aldım. Sadece son satırını aşağıdaki gibi değiştirdim:
[code language=”javascript”]
return “#” + d2h(Math.round(r * 255)) + d2h(Math.round(g * 255)) + d2h(Math.round(b * 255));
[/code]
Amacım renk kodunu #ff23ff şeklinde RGB formatında elde etmekti. He renk için iki karakter kullanmak gerektiğinden de yardımcı olarak d2h (onluk düzenden onaltılık düzene çevirme) adlı bir fonksiyon kullandım (Fonksiyonu 3 numaralı kaynakta buldum).
[code language=”javascript”]
function d2h(d) {
var s = (+d).toString(16);
if (s.length < 2) {
s = ‘0’ + s;
}
return s;
}
[/code]
Tüm program linkte açılan sayfanın kaynak kodundan alınabilir. Bütün javascript rutinleri de sayfanın içindedir.