Fractals and Tiles (com html5) – Parte 6 – Barnsley’s Fern

Publicado em 24/10/2011

0


Olá pessoal, tudo certo?!

No post de hoje apresento os fundamentos para geração de um fractal muito famoso. Trata-se do “Barney’s Fern”. Um dos mais belos, IMHO.

image

Nossa demonstração permite que o usuário mude o tamanho da “viewport” alterando a posição dos marcadores. Basta selecionar o marcador que desejamos mover (com um clique), depois clicar na posição desejada (onde queremos que o marcador fique).

Live code: http://users.cjb.net/livedemoelemarjr/barnsley_chaos.htm

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

Codificando o modelo matemático

Barnsley’s Fern é implementado através de um “chaos game” (assim como o que implementamos no post anterior). Trata-se de um algoritmo IFS, onde as “âncoras” estão indicadas abaixo (peguei a imagem do artigo da wikipedia).


f(x,y) = \begin{bmatrix} \ 0.00 & \ 0.00 \ \\ 0.00 & \ 0.16 \end{bmatrix} \begin{bmatrix} \ x \\ y \end{bmatrix}


f(x,y) = \begin{bmatrix} \ 0.85 & \ 0.04 \ \\ -0.04 & \ 0.85 \end{bmatrix} \begin{bmatrix} \ x \\ y \end{bmatrix} + \begin{bmatrix} \ 0.00 \\ 1.60 \end{bmatrix}


f(x,y) = \begin{bmatrix} \ 0.20 & \ -0.26 \ \\ 0.23 & \ 0.22 \end{bmatrix} \begin{bmatrix} \ x \\ y \end{bmatrix} + \begin{bmatrix} \ 0.00 \\ 1.60 \end{bmatrix}


f(x,y) = \begin{bmatrix} \ -0.15 & \ 0.28 \ \\ 0.26 & \ 0.24 \end{bmatrix} \begin{bmatrix} \ x \\ y \end{bmatrix} + \begin{bmatrix} \ 0.00 \\ 0.44 \end{bmatrix}

Abaixo, reproduzo essa sequência de matrizes em código. Observe:


              
function BarnsleyFern() {
    this.nthPoint = {
        x: 0,
        y: 0
    };

    this.f_1 = function () {
        this.nthPoint.x = 0;
        this.nthPoint.y *= 0.16;
    };

    this.f_2 = function () {
        var oldX = this.nthPoint.x;
        var oldY = this.nthPoint.y;
        this.nthPoint.x = 0.85 * oldX + 0.04 * oldY;
        this.nthPoint.y = -0.04 * oldX + 0.85 * oldY + 1.6;
    }

    this.f_3 = function () {
        var oldX = this.nthPoint.x;
        var oldY = this.nthPoint.y;
        this.nthPoint.x = 0.2 * oldX + -0.26 * oldY;
        this.nthPoint.y = 0.23 * oldX + 0.22 * oldY + 1.6;
    }

    this.f_4 = function () {
        var oldX = this.nthPoint.x;
        var oldY = this.nthPoint.y;
        this.nthPoint.x = -0.15 * oldX + 0.28 * oldY;
        this.nthPoint.y = 0.26 * oldX + 0.24 * oldY + 0.44;
    }

    this.iterate = function () {
        var rand = Math.floor(Math.random() * 100);
        if (rand <= 3)
            this.f_1();
        else if (rand <= 76)
            this.f_2();
        else if (rand <= 90)
            this.f_3();
        else if (rand >= 90)
            this.f_4();
        return this.nthPoint;
    }
}
var fern = new BarnsleyFern();

            

Como você pode ver, nossa pequena classe mantém quatro funções correspondendo àquelas que indiquei acima. O método iterate aplica transformações sequenciais em um único ponto que mantemos em registro. Aqui, a mágica de fato acontece.

Definindo a viewport

Chamo de viewport a área destinada a desenho no canvas (indicada por um retângulo). Escrevi uma classe, simples, para armazenar informações relacionadas a viewport. Observe:


              
var MathHelper = {
    distance: function (x1, y1, x2, y2) {
        var dx = x2 - x1;
        var dy = y2 - y1;
        return Math.sqrt(dx * dx + dy * dy);
    }
};

// -----------------------------------------------------
function BarnsleyViewport(w, h) {
    var margin = 40;
    var size = Math.min(w, h) - 40;

    this.points = new Array(2);
    this.points[0] = {
        x: w / 2 - size / 4,
        y: h / 2 - size / 2
    };

    this.points[1] = {
        x: w / 2 + size / 4,
        y: h / 2 + size / 2
    };

    this.moving = 0;

    this.getLimits = function () {
        with (this) {
            return {
                min: {
                    x: Math.min(points[0].x, points[1].x),
                    y: Math.min(points[0].y, points[1].y)
                },
                max: {
                    x: Math.max(points[0].x, points[1].x),
                    y: Math.max(points[0].y, points[1].y)
                }
            };
        }
    };

    this.pickPoint = function (x, y) {
        if (MathHelper.distance(x, y, this.points[0].x, this.points[0].y) < 10)
            return 0;

        if (MathHelper.distance(x, y, this.points[1].x, this.points[1].y) < 10)
            return 1;

        return -1;
    };

    this.project = function (p) {
        var limits = this.getLimits();
        var w = limits.max.x - limits.min.x;
        var h = limits.max.y - limits.min.y;
        return {
            x: p.x * (w / 5) + limits.min.x + ((2.18 / 5) * w),
            y: p.y * (h / 10) + limits.min.y
        }
    };
}

            

Alguns pontos a observar:

  • Com algum refactoring, senti a necessidade de manter uma pequena classe utilitária para calcular a distância entre dois pontos. Por isso, MathHelper.
  • Como você pode perceber, mantenho dois pontos para delimitar a viewport. Quando uma instância é iniciada, defino duas coordenadas que criam um retângulo com a mesma proporção indicada pelo algoritmo (onde a altura é duas vezes maior que a largura);
  • O método getLimits retorna um objeto definindo os limites da viewport. Perceba que esses limites podem ser alterados conforme a interação do usuário;
  • O método pickPoint será utilizado na seleção dos pontos;
  • O método project converte um ponto gerado pela classe BarnsleyFern para coordenadas de tela. Os critérios são simples:
    • como os valores y gerados pelo algoritmo estão entre 0 e 10, escalo esse ponto para a altura da viewport.
    • como os valores x estão entre –2.18 e 2.82, escalo esse ponto e “zero” o ponto adicionando o offset.

Expandindo Canvas com alguns métodos de apoio: classe CanvasAdapter

Diferente do que fiz nos posts anteriores, resolvi “isolar” meus métodos de desenho. Esse código é “html5 specific”. Observe:


              
function CanvasAdapter(canvas, context) {
    this.canvas = canvas;
    this.context = context;

    this.canvasWidth = canvas.width;
    this.canvasHeight = canvas.height;

    this.clear = function () {
        this.context.clearRect(0, 0,
            this.canvasWidth,
            this.canvasHeight
            );
    };

    this.drawLine = function (x1, y1, x2, y2) {
        with (this.context) {
            beginPath();
            moveTo(x1, this.canvasHeight - y1);
            lineTo(x2, this.canvasHeight - y2);
            closePath();
            stroke();
        }
    };

    this.drawPoint = function (p) {
        this.drawLine(p.x, p.y, p.x + 1, p.y);
    };

    this.drawMarker = function (x, y) {
        this.drawLine(x - 10, y, x + 10, y);
        this.drawLine(x, y - 10, x, y + 10);
    };

    this.drawFrame = function (x1, y1, x2, y2, moving) {
        with (this) {
            context.strokeStyle = "#eee";
            drawLine(x1, y1, x2, y1);
            drawLine(x2, y1, x2, y2);
            drawLine(x2, y2, x1, y2);
            drawLine(x1, y2, x1, y1);

            context.strokeStyle = 0 == moving ? "#f00" : "#000";
            drawMarker(x1, y1);
            context.strokeStyle = 1 == moving ? "#f00" : "#000";
            drawMarker(x2, y2);
        }
    };
}

            

and .. action!

Já temos todo código de apoio que precisamos. Agora, vamos implementar o “entry point”. Observe:


              
var canvas = $('#target');
var context = canvas.get(0).getContext('2d');
var ca = new CanvasAdapter(canvas, context);

// -----------------------------------------------------

var vp;
function resizeCanvas() {
    canvas.attr("width", $(window).get(0).innerWidth);
    canvas.attr("height", $(window).get(0).innerHeight);
    ca.canvasWidth = canvas.width();
    ca.canvasHeight = canvas.height();
    vp = new BarnsleyViewport(ca.canvasWidth, ca.canvasHeight);
    ca.clear();
    playGame();
};
$(window).resize(resizeCanvas);
resizeCanvas();

// -----------------------------------------------------

$("#target").click(function (e) {
    var nx = e.pageX;
    var ny = ca.canvasHeight - e.pageY;

    if ((picked = vp.pickPoint(nx, ny)) != -1)
        vp.moving = picked;
    else {
        vp.points[vp.moving].x = nx;
        vp.points[vp.moving].y = ny;
        ca.clear();
    }
});

// -----------------------------------------------------

function playGame() {
    ca.context.strokeStyle = "#0f0";
    for (var i = 0; i < 100; i++) {
        ca.drawPoint(vp.project(fern.iterate()));
    }
    ca.drawFrame(vp.points[0].x, vp.points[0].y, vp.points[1].x, vp.points[1].y, vp.moving);
    setTimeout(playGame, 33);
}
});

            

O que podemos dizer?!

  • Como usual, monitoro os redimensionamentos da janela para forçar a atualização do tamanho do canvas;
  • Simplifiquei drasticamente a rotina para seleção de pontos (compare com o post anterior);
  • Como já usual, implementei a rotina de desenho de forma que 100 pontos sejam desenhados a cada frame (são ~30 FPS).

Era isso!

Smiley piscando

Publicado em: Post