Fractals and Tiles (com html5) – Parte 2 – Snowflakes

Posted on 20/10/2011

1


Olá pessoal, tudo certo?!

No post anterior, apresentei uma definição, não rigorosa, para fractal. Além disso, mostrei um exemplo simples de Tree (um exemplo simples de fractal).

Hoje, mostro a implementação para Snowflakes.

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

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

O que é snowflake?

Um snowflake é uma curva geométrica e um dos primeiros “fractais” a ser descrito. Para ser construído, possui dois elementos fundamentais:

  1. initiator – uma forma geométrica básica que funciona como ponto de partida;
  2. generator – conjunto de alterações a ser aplicado na reta.

Abaixo, temos um exemplo:

O fractal funciona através da substituição de cada segmento de reta do initiator pelo generator. O resultado é como o que segue:

image

 

Sendo que o processo pode ser repetido recursivamente. O resultado com dois níveis é:

image

Quatro níveis:

image

Representando Generator e Initiator

No exemplo que estou apresentando hoje, começo definindo uma “classe” para cada “tipo” de snowflake que pretendo representar.

O primeiro snowflake que defini é conhecido como snowflake de Koch. Foi o que usei no exemplo acima. Observe:


              
function KochSnowflake() {
    this.generator = {
        scaleFactor: 1 / 3,
        dTheta: [
                0,
                Math.PI / 3,
                -2 * Math.PI / 3,
                Math.PI / 3
            ]
    };

    this.initiator = {
        x: [],
        y: []
    }

    this.update = function (size) {

        h = Math.tan(DTR(60)) * (size / 2);
        this.initiator.x[0] = -size / 2;
        this.initiator.x[1] = 0;
        this.initiator.x[2] = +size / 2;

        this.initiator.y[0] = -h / 2;
        this.initiator.y[1] = +h / 2;
        this.initiator.y[2] = -h / 2;
    }
}

            

A idéia é simples. Mantenho uma “propriedade” para o generator, outra para o initiator.

Sendo que:

  • No generator, mantenho uma coleção de ângulos que desejo aplicar (transformações) na aresta. Além disso, mantenho um “fator” com a estratégia para “fracionar” a linha (em três partes, nesse exemplo).
  • No initiator, mantenho uma lista de coordenadas que atualizo conforme o usuário seleciona um novo tamanho.

Abaixo, apresento um outro snowflake, conhecido como anti koch. Basicamente, espelho para baixo o generator. Observe:


              
function AntiKochSnowflake() {
    this.generator = {
        scaleFactor: 1 / 3,
        dTheta: [
                0,
                -Math.PI / 3,
                2 * Math.PI / 3,
                -Math.PI / 3
            ]
    };

    this.initiator = {
        x: [],
        y: []
    }

    this.update = function (size) {

        h = Math.tan(DTR(60)) * (size / 2);
        this.initiator.x[0] = -size / 2;
        this.initiator.x[1] = 0;
        this.initiator.x[2] = +size / 2;

        this.initiator.y[0] = -h / 2;
        this.initiator.y[1] = +h / 2;
        this.initiator.y[2] = -h / 2;
    }
}

            

Abaixo, você percebe esse snowflake com 5 níveis. Observe:

image

Bacana, não.

Agora, um último padrão. Trata-se do Gosper. Observe:


              
function GosperSnowflake() {
    this.generator = {
        scaleFactor: 0.4472136,
        dTheta: [
                DTR(26.565051),
                -Math.PI / 2,
                Math.PI / 2
            ]
    };

    this.initiator = {
        x: [],
        y: []
    }

    this.update = function (size) {
        size /= 2;
        angle = 0;
        for (var i = 0; i < 6; i++) {
            this.initiator.y[i] = size * Math.sin(angle);
            this.initiator.x[i] = size * Math.cos(angle);
            angle += DTR(60);
        }
    }
}

            

O initiator desse snowflake é um hexágono. Observe:

image

Abaixo, cinco níveis.

image

Assinando eventos

O código que mostro abaixo assina os eventos de interface com o usuário. Acho que não preciso dar muitos detalhes. Perceba:


              
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');

var canvasWidth = 480;
var canvasHeight = 480;
var currentSnowflake = new KochSnowflake();

$("#kochButton").click(function () {
    currentSnowflake = new KochSnowflake();
    update();
});

$("#antikochButton").click(function () {
    currentSnowflake = new AntiKochSnowflake();
    update();
});

$("#gosperButton").click(function () {
    currentSnowflake = new GosperSnowflake();
    update();
});

// -----------------------------------------------------
$("#depthSlider").slider(
{
    min: 0, max: 5, value: 0, animate: true,
    change: function (e, ui) { update(); }
});


$("#sizeSlider").slider(
{
    min: 5, max: 600, step: 0.01, value: 300, animate: true,
    change: function (e, ui) { update(); }
});

$("#centerXSlider").slider(
{
    min: 0, max: 480, value: 240, animate: true,
    change: function (e, ui) { update(); }
});

$("#centerYSlider").slider(
{
    min: 0, max: 480, value: 240, animate: true, orientation: "vertical",
    change: function (e, ui) { update(); }
});

            

Gostaria de destacar apenas que mantenho uma instância do “tipo” de snowflake que desejo aplicar na variável currentSnowFlake.

Em qualquer mudança, chamo o método update.

Implementando a geração e desenho

Temos os dados e a relação com a interface. Basta aplicar as rotinas de desenho propriamente ditas. ObserveSmiley surpreso

  


              
function drawLine(x1, y1, x2, y2) {
    with (context) {
        beginPath();
        moveTo(getCenterX() + x1, canvasHeight - (getCenterY() + y1));
        lineTo(getCenterX() + x2, canvasHeight - (getCenterY() + y2));
        closePath();
        stroke();
    }
}

function drawFlakeEdge(depth, x, y, theta, dist) {
    if (depth <= 0) {
        x2 = x + dist * Math.cos(theta);
        y2 = y + dist * Math.sin(theta);
        drawLine(x, y, x2, y2);
    }
    else {
        with (currentSnowflake.generator) {
            dist = dist * scaleFactor;
            for (var i = 0; i < dTheta.length; i++) {
                theta = theta + dTheta[i];
                x2 = x + dist * Math.cos(theta);
                y2 = y + dist * Math.sin(theta);
                drawFlakeEdge(depth - 1, x, y, theta, dist);
                x = x2;
                y = y2;
            }
        }
    }
}

function drawSnowFlake() {
    with (currentSnowflake.initiator) {
        for (var i = 0; i < x.length; i++) {
            var x1 = x[i];
            var y1 = y[i];

            var x2 = x[(i + 1) % x.length];
            var y2 = y[(i + 1) % x.length];

            var dx = x2 - x1;
            var dy = y2 - y1;
            var theta = Math.atan2(dy, dx);

            length = Math.sqrt(dx * dx + dy * dy);

            drawFlakeEdge(getDepth(), x1, y1, theta, length);
        }
    }
}

function drawZero() {
    drawLine(-10, 0, 10, 0);
    drawLine(0, -10, 0, 10);
}

function DTR(x) {
    return x * Math.PI / 180;
}

function update() {
    context.clearRect(0, 0, canvasWidth, canvasHeight);
    currentSnowflake.update(getSize());
    drawZero();
    drawSnowFlake();
}

update();


            

O que gostaria de destacar? Bem…

  1. Criei uma função para desenhar linhas (drawline) que desloca a origem (ponto 0,0) para a posição indicada pelo usuário na interface. Além disso, esse mesmo método inverte o sentido do eixo Y;
  2. A função drawZero desenha uma marca no canvas indicando a posição da origem;
  3. drawSnowFlake orquestra o desenho das arestas de initiator;
  4. drawFlakeEdge processa o desenho de cada aresta. Observe que é um método recursivo (e bonito, na minha opinião). Repare que as retas vão sendo fracionadas em cada nível até que seja atingido o ponto de desenho.

Era isso.

Posted in: Post