Fractals and Tiles (com html5) – Parte 7 – Mandelbrot set

Publicado em 25/10/2011

1


Olá pessoal, tudo certo?!

Finalmente, chego ao mais famoso dos fractais. Trata-se do conjunto de Mandelbrot.

image

Código-fonte: https://gist.github.com/1314575

Live demo: http://users.cjb.net/livedemoelemarjr/mandelbrot.htm

Importante: A performance do demo é assustadoramente ruim no Internet Explorer 9. Confesso que isso me pega de surpresa e ainda acho que escrevi alguma coisa “muito errado”. Recomendo que você rode o demo no Chrome e/ou Firefox.

Usando o demo

Na demo, o usuário pode selecionar entre três tamanhos de viewport. Depois, pode selecionar uma região da imagem para renderizar de forma ampliada.

Para dar reset, basta clicar novamente no botão de tamanho.

Abaixo, algumas imagens:

image

image

image

image

Nosso objeto Mandelbrot

Embora tenha aparência extremamente complexa, é originado de uma equação muito simples.

z_0 = 0\,

z_{n+1} = {z_n}^2 + c

Onde, Zn e C são números complexos.

Apenas com uma breve revisão, um número complexo tem uma parte “real” e uma parte “imaginária”. Eles são escritos com um componente real R e uma parte imaginária I (R + I * i), onde i é um número especial representando a raiz quadrada de –1.

Como você pode perceber, nossa equação indica, mais uma vez, uma recursão.

Uma excelente fundamentação pode ser encontrada em http://mathworld.wolfram.com/MandelbrotSet.html

Abaixo, apresento uma implementação para a resolução desse fractal. Os parâmetros qmin, qmax, pmin e pmax definem um “intervalo” de trabalho. É através desses parâmetros que modifico a imagem que está sendo desenhada.


              
Mandel = {
        iterLimit: 100,
        qmin: -1.5,
        qmax: 1.5,
        pmin: -2.25,
        pmax: 0.75,

        reset: function () {
                Mandel.iterLimit = 100;
                Mandel.qmin = -1.5;
                Mandel.qmax = 1.5;
                Mandel.pmin = -2.25;
                Mandel.pmax = 0.75;
        },

        compute: function () {
                var kmax = 256;
                var x = 0.0;
                var y = 0.0;
                var r = 1.0;

                mandelImage = context.getImageData(0, 0, canvasWidth, canvasHeight);
                mandelPixels = mandelImage.data;

                var xstep = (Mandel.pmax - Mandel.pmin) / canvasWidth;
                var ystep = (Mandel.qmax - Mandel.qmin) / canvasHeight;
                for (var sx = 0; sx < canvasWidth; sx++) {
                        for (var sy = 0; sy < canvasHeight; sy++) {
                                var p = Mandel.pmin + xstep * sx;
                                var q = Mandel.qmax - ystep * sy;
                                var k = 0;
                                var x0 = 0.0;
                                var y0 = 0.0;

                                do {
                                        x = x0 * x0 - y0 * y0 + p;
                                        y = 2 * x0 * y0 + q;
                                        x0 = x;
                                        y0 = y;
                                        r = x * x + y * y;
                                        k++;
                                }
                                while ((r <= Mandel.iterLimit) && (k < kmax));

                                if (k >= kmax) {
                                        k = 0;
                                }

                                drawPixel(mandelPixels, sx, sy, k);
                        }
                }
                context.putImageData(mandelImage, 0, 0);
        }

};

            

Repare que, no lugar de pintar diretamente no canvas, faço o “desenho” em um bitmap. Isso garante uma boa performance.

A seleção das cores que vão ser empregadas também é muito importante. Abaixo, mostro um “método clássico” para calcular um conjunto de cores.


              
MandelColors = {
        controlColors: new Array(5),
        colors: new Array(512),

        reset: function () {
                with (MandelColors) {
                        controlColors[0] = [0x00, 0x00, 0x20];
                        controlColors[1] = [0xff, 0xff, 0xff];
                        controlColors[2] = [0x00, 0x00, 0xa0];
                        controlColors[3] = [0x40, 0xff, 0xff];
                        controlColors[4] = [0x20, 0x20, 0xff];
                }
        },

        compute: function () {
                with (MandelColors) {
                        var i, k;
                        colors[0] = [0, 0, 0];

                        for (i = 0; i < 4; i++) {
                                var rstep = (controlColors[i + 1][0] - controlColors[i][0]) / 63;
                                var bstep = (controlColors[i + 1][1] - controlColors[i][1]) / 63;
                                var gstep = (controlColors[i + 1][2] - controlColors[i][2]) / 63;

                                for (k = 0; k < 64; k++) {
                                        colors[k + (i * 64) + 1] = [
                                        Math.round(controlColors[i][0] + rstep * k),
                                        Math.round(controlColors[i][1] + bstep * k),
                                        Math.round(controlColors[i][2] + gstep * k)];
                                }
                        }

                        for (i = 257; i < 512; i++) {
                                colors[i] = colors[i - 256];
                        }
                }
        }
};

            

Repare que mantenho a “interface” que havia utilizado no objeto Mandel.

Você pode obter resultados fantásticos com cores mais vibrantes, modificando esse objeto.

Por fim, mostro a rotina que coloca as cores no mapa de pixel. Observe:


              
function drawPixel(mandelPixels, x, y, c) {
        var offset = 4 * (y * canvasWidth + x);
        mandelPixels[offset] = MandelColors.colors[c][0];
        mandelPixels[offset + 1] = MandelColors.colors[c][1];
        mandelPixels[offset + 2] = MandelColors.colors[c][2];
        mandelPixels[offset + 3] = 255;
}

            

Implementando a seleção de tamanho para viewport

Gostei de implementar a opção para mudar o tamanho da viewport. Observe:


              
var canvas = $("#target");
var context = canvas.get(0).getContext("2d");
var canvasWidth;
var canvasHeight;

$("#presets").buttonset();
$("#r300").click(function () { load(300, 300); });
$("#r450").click(function () { load(450, 450); });
$("#r600").click(function () { load(600, 600); });

function load(w, h) {
        $("#target").animate(
                {
                        width: w + "px",
                        height: h + "px"
                },
                200
        );
        canvas.get(0).width = w;
        canvas.get(0).height = h;

        canvasWidth = w;
        canvasHeight = h;
        with (canvas.get(0)) {
                left = offsetLeft;
                top = offsetTop;
        }

        MandelColors.reset();
        MandelColors.compute();
        Mandel.reset();
        Mandel.compute();
}
load(300, 300);

            

Implementei a mudança de tamanho com uma pequena animação. Infelizmente, no IE, devido ao processamento pesado não é possível ver a transição. Entretanto, o efeito funciona perfeitamente no Firefox e no Chrome.

Implementando a seleção de uma região (com suporte para o iPhone e iPad)

Para terminar, gostaria de mostrar como implementei a seleção de uma área da imagem. Observe:


              
var selecting = false;
var mbX = 0;
var mbY = 0;
var meX = 0;
var meY = 0;
var left = 0;
var top = 0;
var backImage;

function onStartSelect(e) {
        e = window.event || e;
        coords = getCoords(e);
        mbX = coords.x;
        mbY = coords.y;
        backImage = context.getImageData(0, 0, canvasWidth, canvasHeight);
        selecting = true;
}

function onSelectArea(e) {
        if (!selecting) return;

        e = window.event || e;
        coords = getCoords(e);

        meX = coords.x;
        meY = mbY +
                ((coords.y > mbY ? 1 : -1)) *
                Math.round(canvasHeight * Math.abs(coords.x - mbX) / canvasWidth);

        context.putImageData(backImage, 0, 0);
        context.strokeStyle = "#f00";
        context.strokeRect(mbX, mbY, meX - mbX, meY - mbY);
}


function onEndSelect(e) {
        if (!selecting) return

        var x1 = Math.min(mbX, meX);
        var y1 = Math.min(mbY, meY);
        var x2 = Math.max(mbX, meX);
        var y2 = Math.max(mbY, meY);

        if ((Math.abs(x2 - x1) > 3) && (Math.abs(y2 - y1) > 3)) {
                with (Mandel) {
                        var pw = pmax - pmin;
                        pmin = pmin + x1 * pw / canvasHeight;
                        pmax = pmax - (canvasWidth - x2) * pw / canvasWidth;
                        var qw = qmax - qmin;
                        qmin = qmin + (canvasHeight - y2) * qw / canvasHeight;
                        qmax = qmax - y1 * qw / canvasHeight;
                        compute();
                }

        }

        selecting = false;
}

function getCoords(e) {
        if (e.offsetX)
                return { x: e.offsetX, y: e.offsetY };

        return { x: e.pageX - canvas.get(0).offsetLeft, y: e.pageY - canvas.get(0).offsetTop };
}

function init() {
        with (canvas.get(0)) {
                onmousedown = onStartSelect;
                onmousemove = onSelectArea;
                onmouseup = onEndSelect;
                addEventListener("touchstart", onStartSelect, false);
                addEventListener("touchmove", onSelectArea, false);
                addEventListener("touchend", onEndSelect, false);
        }
        document.body.addEventListener('touchmove', function (event) {
                event.preventDefault();
        }, false);
}
init();

            

Nada de muito especial, exceto, suporte ao touch do iPad. Como pode ver, assino os eventos de touch. Além disso, evito o “scroll” automático através da evocação do método preventDefault.

Era isso.

Smiley piscando

Publicado em: Post