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.
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).
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!
Publicado em 24/10/2011
0