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