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:
- initiator – uma forma geométrica básica que funciona como ponto de partida;
- 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:
Sendo que o processo pode ser repetido recursivamente. O resultado com dois níveis é:
Quatro níveis:
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:
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:
Abaixo, cinco níveis.
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. Observe
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…
-
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;
-
A função drawZero desenha uma marca no canvas indicando a posição da origem;
-
drawSnowFlake orquestra o desenho das arestas de initiator;
-
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.
outubro 22nd, 2011 → 11:41
[...] parte 2, mostrei uma implementação simples para snowflakes. Na parte 1, mostrei como desenhar trees. [...]