JavaScript & canvas - Mandelbrotova množina

JavaScript Základní konstrukce JavaScript & canvas - Mandelbrotova množina

Mandelbrotova množina je jeden z nejznámějších fraktálů. Jedná se o fraktál ležící v komplexní rovině od [-2;-2] do [2;2]. Vzorec pro jeho výpočet je Z = Z2 + C. Z a C jsou komplexní čísla, kde C je konstantní a jedná se o pozici bodu, ze kterého fraktál počítáme. Postup a podrobnější popis je v článku Mandelbrotova množina, který doporučuji přečíst. Tak a my můžeme začíst programovat.

Vytvoříme si objekt MandelbrotovaMnozina, do kterého uložíme pár proměnných.

var MandelbrotovaMnozina = function() {
        // Soukromé proměnné
        var platno = this.platno = $('#platno').get(0);
        var kontext = platno.getContext('2d');
        var width = platno.width,
            height = platno.height;

        // Vytvoříme si objekt imageData, kde budou data o jednotlivých pixelech
        this.imageData = kontext.createImageData(width, height);
        this.maxiter = 20;
}

Proměnná platno obsahuje element plátna. V tomto případě jsem použil jQuery, ale lze to udělat i přes klasický DOM a to document.getElementById("platno"). Další proměnná je 2D kontext na kreslení a pak rozměry plátna. Základní práce s canvasem je vysvětlená v článku Canvas aneb grafika JavaScriptem. Zajímavější jsou ale veřejné proměnné. V proměnné imageData je uložen objekt ImageData, který vrací metoda kontext.create­ImageData(wid­th, height); Vytvoříme si tak pole pixelů na plátně. Nakonec maxiter je proměnná určující maximální počet iterací (maximální počet pokusů na vyzkoušení, jestli daný bod opravdu patří do množiny).

Metoda pro manipulaci s pixely

JavaScript nemá přímo metodu, která by uměla na uživatelsky přijatelné úrovni manipulovat s pixely na plátně, a proto si ji musíme vytvořit sami. Nazveme ji třeba navstavPixel, ještě než ji ale vysvětlím, pokusím se definovat pixel. Pixel je nějaký bod na plátně. Zde je tvořen souborem subpixelů, což jsou jednotlivé složky barev a alfa kanál. Jeden pixel je tak v poli vyjádřen čtyřmi čísli. První je hodnota červené, druhý hodnota zelené, třetí modré a čtvrtý je právě ten alfa kanál (průhlednost). Tohle je důležité si uvědomit. Jdeme na to.

// Funkce nastaví barvu pro daný pixel
this.nastavPixel = function(x, y, r, g, b, a) {
        // Zjistíme pozici v poli (pozici červeného subpixelu)
        var index = (x + y * this.imageData.width) * 4;
        // Hodnota červené bervy
        this.imageData.data[index+0] = r;
        // Hodnota zelené barvy
        this.imageData.data[index+1] = g;
        // Hodnota modré barvy
        this.imageData.data[index+2] = b;
        // Alfa kanál (průhlednost)
        this.imageData.data[index+3] = a;
};

Vstup funkce bude bod x, y, hodnota červené, hodnota zelené, hodnota modré a alfa kanál. Index je pozice červeného (prvního) subpixelu. Musíme si ho vypočítat, jelikož máme každý pixel vyjádřený čtyřmi čísly. Pak jen nastavujeme jednotlivé složky daného pixelu, jak se sami můžete přesvědčit.

Metoda pro vykreslení fraktálu

Funkce bude mít tři vstupní parametry. Jednak prostřední x komplexního čísla, prostřední y komplexního čísla a pak přiblížení. Deklarujeme také proměnné potřebné k vykreslení fraktálu.

// Funkce vykreslí Mandelbrotovu množinu
this.vykresli = function( xCenter, yCenter, zoom ) {
        // Uloží zoom
        $(platno).data('zoom', zoom);
        // Vytvoří nová image data
        this.imageData = kontext.createImageData(width, height);

        // Potřebné proměnné
        var count, zr, zi, zr2, zi2;
        // Přiblížení
        var zoom = zoom > 1 ? zoom*3 : zoom;
        // Y, od kterého začneme
        var minY = yCenter - (1.5/(zoom));
        // Y, u kterého skončíme
        var maxY = yCenter + (1.5/(zoom));
        // X, u kterého začneme
        var minX = xCenter - (1.5/(zoom));
        // X, u kterého skončíme
        var maxX = xCenter + (1.5/(zoom));
        // Vypočteme jeden krok cyklu for X
        var dx = (maxX - minX) / width;
        // Vypočteme jeden krok cyklu for Y
        var dy = (maxY - minY) / height;
        // Budeme ukládat reálnou pozici pixelu na plátně
        var rX = 0, rY = 0;
}

Když máme deklarované proměnné, můžeme začít vykreslovat. Musíme projít všechny pixely a tak budeme potřebovat dva do sebe vnořené cykly for (jeden pro X a jeden pro Y). V každém kroku vynulujeme naše komplexní číslo Z. Vzorec pro výpočet je pak následující: Reálná část = zr2 - zi2 Imaginární část: zi * zr + zi * zr = 2 * Zr * Zi

Abychom se v tom neztráceli, raději si vytvoříme proměnné druhé mocniny reálné a imaginární části komplexního čísla Z. Uložíme si je do proměnných zr2, zi2. V cyklu budeme toto počítat dokud nebudeme mimo množinu (součet druhé mocniny reálné a imaginární části bude větší než čtyři) nebo přesáhneme maximální počet pokusů (iterací). Pokud budeme mimo množinu, obarvíme si pixel podle počtu pokusů. Jinak obarvíme pixel černě.

// Projdeme všechny pixely na plátně
for(var y = minY; y < maxY; y += dy) {
        // Přičteme reálnou pozici Y
        rY++;
        // Jsme v novém řádku => vynulujeme widthCheck
        rX = 0;
        // Projdeme X a zároveň se ujistíme, že jsem stále na plátně
        for (var x = minX; x < maxX && rX < width; x += dx) {
                // Přičteme reálnou pozici X
                rX++;
                // Vynulujeme komplexní číslo Z a jeho druhou množinu
                zi = 0;
                zr = 0;
                zi2 = 0;
                zr2 = 0;
                // Nastavíme počet provedených iterací na 0
                count = 0;
                // Obarvíme pixel na černo
                this.nastavPixel(rX, rY, 0, 0, 0, 255);

                // Pokud jsme v množině a nevyčerpali jsme možný počet pokusů
                while ((zr2 + zi2) < 4 && count < this.maxiter) {
                        // Uložíme si druhou mocninu reálné části (alernativa: zr*zr)
                        zr2 = Math.pow(zr, 2);
                        // Uložíme si druhou mocninu imaginární části (alternativa: zi*zi)
                        zi2 = Math.pow(zi, 2);
                        // Vzorec: Z.i = 2 * Z.r * Z.i + posunY
                        zi = 2*zr*zi + y;
                        // Vzorec: Z.z^2 - Z.i^2 + posunX
                        zr = zr2 - zi2 + x;
                        // Přičteme počet provedených pokusů
                        count++;
                }

                // Pokud jsme mimo množinu, obarvíme pixel na nějakou barvu podle počtu provedených pokusů
                if ((zr2 + zi2) > 4)
                        //Obarvíme pixel podle počtu provedených pokusů
                        this.nastavPixel(rX, rY, count * 12, count * 4, count * 3, 255);
                }
        }
}

A je hotovo. Teď už můžeme fraktál vykreslit a popřípadě přidat možnost přiblížit fraktál po kliknutí na plátno. To by v jQuery mohlo vypadat třeba

var manMnozina = new MandelbrotovaMnozina();
manMnozina.vykresli(0, 1.5, 0.5);
$(manMnozina.platno).click(function(e) {
    e = $.event.fix(e);
    var left = e.pageX-$(this).offset().left,
        top = e.pageY-$(this).offset().top,
        zoom = $(this).data('zoom')+1;
    manMnozina.vykresli(left/$(this).width(), top/$(this).height(), zoom);
});

Výsledek bude něco podobného tomuto obrázku:

Mandelbrotova množina

 

Stáhnout

Staženo 576x (35.93 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

  Aktivity (1)

Článek pro vás napsal Drahomír Hanák
Avatar
Autor v současné době studuje Informatiku. Zajímá se o programování, matematiku a grafiku.

Jak se ti líbí článek?
Celkem (7 hlasů) :
4.428574.428574.428574.42857 4.42857


 



 

 

Komentáře

Avatar
David Čápka
Tým ITnetwork
Avatar
David Čápka:

Opět dobrý článek, jen jsem u toho svého asi špatně napsal interace místo iterace a ty jsi to potom převzal, za to se omlouvám :(

Jinak ten tvůj render je stále takový okousaný, ale na první pohled nevidím, čím by to mohlo být. Nastavuješ tam 2 pixely místo jednoho, když barvím jen bod na rX a rY, tak jsou přes množinu černé čáry, rY se však zvyšuje jen jednou v cyklu. Jakto, že se evidentně zvyšuje po dvou?

Odpovědět 30.4.2012 14:15
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Kit
Redaktor
Avatar
Odpovídá na David Čápka
Kit:

Asi proto, že nikde uvnitř prvního cyklu nenuluje rX. Těch chyb je tam asi víc. Nepochopil jsem smysl posunutého pixelu.

Odpovědět 30.4.2012 14:58
Vlastnosti objektů by neměly být veřejné. A to ani prostřednictvím getterů/setterů.
Avatar
Drahomír Hanák
Tým ITnetwork
Avatar
Odpovídá na Kit
Drahomír Hanák:

Ano, to je ta chyba. Napsal jsem si na to tu proměnnou widthCheck a rX, rY jsem přidal až ke konci, takže jsem si nevšiml, že se jaksi rX nevynuluje. Tady už to funguje http://jsfiddle.net/…os/ewNfQ/13/ Stačí přejmenovat widthCheck na rX

 
Odpovědět 30.4.2012 15:10
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovědět 30.4.2012 15:20
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Drahomír Hanák
David Čápka:

Na ty 3 barvy jsem šel tak, že jsem do červené složky dával přímo tu vypočtenou hodnotu barvy podle iterace (c = 0 až 255), do zelené jsem dával (255 - c) a do modré jsem dal něco jako (128 - (c - 128)). Nějak jsem si s tím pak hrál a vypadá to docela dobře, jen tam mám nějaký problém s tím, že to tmavne při příliš velkých iteracích, chtělo by to asi nějak rozpočítávat.

Ten článek editneš nebo ho mám upravit? :)

Odpovědět 30.4.2012 16:05
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Drahomír Hanák
Tým ITnetwork
Avatar
Odpovídá na David Čápka
Drahomír Hanák:

Taky mě to napadlo. Šla by tam přidat funkce na převod RGB barvy, ale musel bych to propočítat :)

Článek upravím, jen ještě musím dodělat pár věcí, abych na ně nezapomněl.

 
Odpovědět 30.4.2012 17:00
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na Drahomír Hanák
David Čápka:

Udělal jsem si z toho 25Mpxl obrázek a ani to netrvalo dlouho, ten canvas je docela pecka, z tohodle by šel i plakát :D

Odpovědět  +1 30.4.2012 19:42
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
Drahomír Hanák
Tým ITnetwork
Avatar
Odpovídá na David Čápka
Drahomír Hanák:

Tak to jsem ani před tím nezkoušel :) Vypadá to dobře, akorát to data URL je trošičku delší :)

 
Odpovědět  +1 30.4.2012 20:01
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 8 zpráv z 8.