Olá pessoal, tudo certo?
Há algum tempo, escrevi um post sobre animações com HTML5. O post de hoje é uma “revisão”. Aproveito para adicionar alguns conceitos mais sólidos que “puxei” do XNA.
O código completo pode ser obtido nesse endereço: https://gist.github.com/1290398. Se preferir, pode ver o código no jsFiddle, em http://jsfiddle.net/ElemarJR/G7h6T/.
Live demo: http://users.cjb.net/livedemoelemarjr/spheres.htm.
Primeiro, fazendo o Canvas “ocupar completamente” o browser
Para que nosso aplicação fique mais interessante, comecemos fazendo com que o Canvas ocupe todo o browser. Eis como fazemos isso. Primeiro, nossa marcação:
Spheres
Agora, algum script:
$(function () { var canvas = $("#target"); var context = canvas.get(0).getContext("2d"); $(window).resize(resizeCanvas); function resizeCanvas() { canvas.attr("width", $(window).get(0).innerWidth); canvas.attr("height", $(window).get(0).innerHeight); }; resizeCanvas(); });
Criando o Animation Loop
Fiquei impressionado positivamente com o padrão de funcionamento do XNA. Está lembrado (que tal dar uma olhada)?!
Trata-se de um “loop infinito”, onde os métodos Update e Draw são executados repetidamente. Resolvi adotar o mesmo modelo. Repare:
$(function () { var canvas = $("#target"); var context = canvas.get(0).getContext("2d"); var canvasWidth = canvas.width(); var canvasHeight = canvas.height(); $(window).resize(resizeCanvas); function resizeCanvas() { canvas.attr("width", $(window).get(0).innerWidth); canvas.attr("height", $(window).get(0).innerHeight); canvasWidth = canvas.width(); canvasHeight = canvas.height(); }; resizeCanvas(); function loadContent() { animate(); } loadContent(); function animate() { update(); draw(); setTimeout(animate, 33); } function update() { } function draw() { } });
Mantenho os mesmos “nomes”. Repare que executo esse método a cada 33 milisegundos, Isso nos dá aproximadamente 30 execuções por segundo (~ 30 FPS)
Iniciando objetos para animação
Já temos nosso target de renderização. Também já temos nosso animation loop. Agora, vamos definir os “modelos” que vamos desenhar.
Agora, implemento o conceito de Javascript Orientado a Objetos (que mostrei em outro post). Perceba:
var Sphere = function (x, y, radius, mass, vX, vY) { this.x = x; this.y = y; this.radius = radius; this.mass = mass; this.vX = vX; this.vY = vY; }
Temos a posição X, Y. Temos o raio de cada esfera, a massa e o vetor para deslocamento. Legal, não!? Agora, vamos “inicializar” esses objetos.
var spheres = new Array(); var spheresLength = 10; function loadContent() { for (var i = 0; i < spheresLength; i++) { var x = 20 + (Math.random() * (canvasWidth - 40)); var y = 20 + (Math.random() * (canvasHeight - 40)); var radius = 5 + Math.random() * 10; var mass = radius / 2; var vX = Math.random() * 4 - 2; var vY = Math.random() * 4 - 2; spheres.push(new Sphere(x, y, radius, mass, vX, vY)); }; animate(); } loadContent();
Basicamente, criei uma 10 esferas e adicionei a nossa coleção.
Desenhando – Implementando o método Draw
Agora, vamos implementar a rotina de desenho. Observe:
function draw() { context.clearRect(0, 0, canvasWidth, canvasHeight); context.fillStyle = "rgb(255, 255, 255)"; for (var i = 0; i < spheresLength; i++) { var sphere = spheres[i]; context.beginPath(); context.arc(sphere.x, sphere.y, sphere.radius, 0, Math.PI * 2); context.closePath(); context.fill(); } }
Perfeito… running…
Dobrando o número de esferas …
Animating…
Já temos nossos “modelos” carregados. Já temos um loop infinito executando métodos de atualização e desenho. Já temos um método de desenho. Agora, vamos implementar a atualização (método Update).
function update() { for (var i = 0; i < spheresLength; i++) { var sphere1 = spheres[i]; for (var j = i + 1; j < spheresLength; j++) { var sphere2 = spheres[j]; var dX = sphere2.x - sphere1.x; var dY = sphere2.y - sphere1.y; var distance = Math.sqrt((dX * dX) + (dY * dY)); if (distance < sphere1.radius + sphere2.radius) { var angle = Math.atan2(dY, dX); var sine = Math.sin(angle); var cosine = Math.cos(angle); var x = 0; var y = 0; var xB = dX * cosine + dY * sine; var yB = dY * cosine - dX * sine; var vX = sphere1.vX * cosine + sphere1.vY * sine; var vY = sphere1.vY * cosine - sphere1.vX * sine; var vXb = sphere2.vX * cosine + sphere2.vY * sine; var vYb = sphere2.vY * cosine - sphere2.vX * sine; var vTotal = vX - vXb; vX = ( (sphere1.mass - sphere2.mass) * vX + 2 * sphere2.mass * vXb ) / (sphere1.mass + sphere2.mass); vXb = vTotal + vX; xB = x + (sphere1.radius + sphere2.radius); sphere1.x = sphere1.x + (x * cosine - y * sine); sphere1.y = sphere1.y + (y * cosine + x * sine); sphere2.x = sphere1.x + (xB * cosine - yB * sine); sphere2.y = sphere1.y + (yB * cosine + xB * sine); sphere1.vX = vX * cosine - vY * sine; sphere1.vY = vY * cosine + vX * sine; sphere2.vX = vXb * cosine - vYb * sine; sphere2.vY = vYb * cosine + vXb * sine; } } sphere1.x += sphere1.vX; sphere1.y += sphere1.vY; if (sphere1.x - sphere1.radius < 0) { sphere1.x = sphere1.radius; sphere1.vX *= -1; } else if (sphere1.x + sphere1.radius > canvasWidth) { sphere1.x = canvasWidth - sphere1.radius; sphere1.vX *= -1; } if (sphere1.y - sphere1.radius < 0) { sphere1.y = sphere1.radius; sphere1.vY *= -1; } else if (sphere1.y + sphere1.radius > canvasHeight) { sphere1.y = canvasHeight - sphere1.radius; sphere1.vY *= -1; } } }
Aqui, há um pouco de física. Mas, basicamente, você precisa entender apenas que verifico se há colisão entre as esferas. Em caso positivo, modifico a “direção” das esferas que estão colidindo. No final, verifico os limites.
Mas, aqui entre nós: “esse código ficou grande”.
Refactoring 1 – Extraindo a verificação de limites no canvas para a classe Sphere
Como havia dito, o código do método Update estava grande. Um pouquinho de orientação a objetos não faz mal. Então:
var Sphere = function (x, y, radius, mass, vX, vY) { this.x = x; this.y = y; this.radius = radius; this.mass = mass; this.vX = vX; this.vY = vY; this.updatePosition = function () { this.x += this.vX; this.y += this.vY; }; this.checkBoundaryCollision = function () { if (this.x - this.radius < 0) { this.x = this.radius; this.vX *= -1; } else if (this.x + this.radius > canvasWidth) { this.x = canvasWidth - this.radius; this.vX *= -1; } if (this.y - this.radius < 0) { this.y = this.radius; this.vY *= -1; } else if (this.y + this.radius > canvasHeight) { this.y = canvasHeight - this.radius; this.vY *= -1; } } }
Você pegou a idéia. É isso!
Michel Pavan Macedo
16/10/2011
Vi a demo fui o demo fui obrigado a ver o post, bacana mesmo as animações.
Dúvida: Tu tens que passar um link adicional para mim entender esse teu método update, porque eu achei muito legal esse efeito, eu lembro de fazer uma brincadeira do tipo quando eu estava aprendendo a programar Delphi (haha, não me culpe, eu só sabia isso), mas isso de usar a massa foi uma sacada bem mais legal.
Dúvida: Outra coisa que eu não entendi é como é que tu usou o método resizeCanvas antes de declará-lo. Javascript de hoje em dia está tudo cheio das mágicas. ;P
Sugestão de utilização do jsFiddle: http://jsfiddle.net/michelpm/G7h6T/1/
Próximo passo: traduzir a série de XNA para WebGL! o/
elemarjr
16/10/2011
Obrigado pelo feedback. Vou dar uma olhada no seu jsfiddle
Michel Pavan Macedo
16/10/2011
http://jsfiddle.net/michelpm/G7h6T/2/
What a shame jsFiddle, o CSS usado por eles matou o teu reset dos padding do body. Adicionei um !important, mas o !important mesmo é que funciona num browser de verdade. ^^