Criando animações bacanas com o Canvas do HTML5

Posted on 15/10/2011

4


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)?!

image

 

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…

image

Dobrando o número de esferas … Smiley de boca aberta

image

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!

Smiley de boca aberta

Etiquetado:, ,
Posted in: Post