Ir para o conteúdo
Dominando o HTML5 Canvas – Parte 1

Dominando o HTML5 Canvas – Parte 1

Isso inicia uma série de postagens de blog sobre como dominar o HTML5 Canvas.

7min read

Vamos nos concentrar primeiro na animação, pois é uma das melhores maneiras de aproveitar a tela para trazer novos tipos de interação e visualizações para seus aplicativos da web. Aqui está uma prévia do que estamos construindo para a parte 1.

Visão geral do Canvas

A tela HTML5 fornece uma API de renderização 2D de modo imediato que podemos usar em nossos aplicativos da web. Não depende de plug-ins de navegador, exigindo apenas um navegador moderno o suficiente que suporte o componente HTML5 Canvas. Mesmo se estiver tentando oferecer suporte a versões mais antigas do Internet Explorer (8 e inferior), existem alguns polyfills que podem ser usados para tentar fornecer suporte a eles (FlashCanvas, exCanvas, etc).

Como a tela é um mecanismo de renderização de modo imediato, em vez de criar um grafo de objeto que represente todos os objetos visíveis, como você faria em um sistema de modo retido como Silverlight ou WPF, conversaremos com o contexto de renderização da tela e, para cada quadro, informaremos quais primitivos visuais renderizar em quais locais.

Hello Circle

Para começar, vamos renderizar um círculo para a tela.

Vamos supor que o jQuery foi referenciado para este aplicativo da web para nos ajudar a manipular os elementos DOM necessários de forma sucinta. Mas o jQuery certamente não é necessário para interagir com a tela. Primeiro, começaremos com o DOM. Precisamos adicionar um elemento de tela em algum lugar em nossa hierarquia DOM onde queremos exibir o conteúdo da tela. Nesse caso, é o único elemento no corpo da minha página.

<canvas id="canvas"></canvas>

Eu forneci um id para que possamos encontrar facilmente esse elemento mais tarde, quando quisermos renderizar seu conteúdo.

Agora é hora de renderizar o círculo na tela. Precisamos começar adquirindo o contexto de renderização 2D da tela. O contexto de renderização é o que mostra a API para nós que precisamos renderizar primitivos na tela.

$(function () {
    var canv, context;
    
    $("#canvas").attr("width", "500px").attr("height", "500px");
    canv = $("#canvas")[0];
    context = canv.getContext("2d");

Aqui estamos usando o jQuery para localizar a tela por id e, em seguida, definindo sua largura e altura. Algo que pode confundi-lo inicialmente é que existem realmente duas larguras e alturas que são relevantes para a tela. O elemento canvas tem atributos de largura e altura que representam o tamanho do bitmap usado para renderizar o conteúdo dentro da tela. Enquanto isso, você também pode definir uma largura e altura css na tela. Eles representam o tamanho para o qual o bitmap gerado pela tela é dimensionado quando exibido na página. Então, por exemplo, se você criasse uma tela de 50px por 50px e a dimensionasse para 500px por 500px, ela pareceria muito em blocos, pois você estaria esticando um quadrado de 50px para preencher uma área quadrada de 500px. Na maioria das vezes, você provavelmente desejará que o tamanho do css seja o mesmo que o tamanho do atributo da tela.

Em seguida, extraímos uma referência ao elemento DOM canvas do jQuery e chamamos getContext("2d") para obter o contexto de renderização. Por que getContext("2d")? Isso ocorre porque a tela HTML5 também desempenha um papel integral no WebGL, além de sua API 2D, mas, por enquanto, estamos falando de 2D.

Em seguida, renderizamos o círculo real. Aqui estou definindo uma função chamada draw que renderizará o círculo e, em seguida, chamando-a para fazer a renderização real. Por que eu dividi isso? Isso se tornará aparente quando adicionarmos animação à mistura.

function draw() {
   context.fillStyle = "rgb(255,0,0)";
   context.beginPath();
   context.arc(150, 150, 120, 0, 2 * Math.PI, false);
   context.fill();
}

draw();

Aqui estamos:

  • Definir o estilo de preenchimento da tela para vermelho. Você pode usar a maioria das sintaxes que uma propriedade css color aceitará aqui.
  • Dizendo ao contexto para iniciar um novo caminho.
  • Desenhar um arco com um centro em 150px, 150px com um raio de 120, varrendo de 0 a 360 graus, no sentido horário (observe que a API realmente quer radianos, não graus)
  • Dizendo ao contexto para preencher o caminho que definimos, preenchendo assim o círculo.

E aqui está o resultado.

Hello Animation

Now, lets animate our circle in some way.

Primeiro, aqui está a lógica modificada da nossa versão anterior:

var canv, context, lastTime, duration, progress, forward = true;
    
$("#canvas").attr("width", "500px").attr("height", "500px");
canv = $("#canvas")[0];
context = canv.getContext("2d");

lastTime = new Date().getTime();

duration = 1000;
progress = 0;

function draw() {
    var elapsed, time, b;
    
    time = new Date().getTime();
    elapsed = time - lastTime;
    lastTime = time;

    if (forward) {
        progress += elapsed / duration;
    } else {
        progress -= elapsed / duration;
    }
    if (progress > 1.0) {
        progress = 1.0;
        forward = false;
    }
    if (progress < 0) {
        progress = 0;
        forward = true;
    }

    b = 255.0 * progress;
    context.fillStyle = "rgb(255," + Math.round(b.toString()) + ",0)";
    context.beginPath();
    context.arc(150, 150, 120, 0, 2 * Math.PI, false);
    context.fill();
}

window.setInterval(draw, 1000 / 60.0);

Então, quais são as diferenças acima?

Primeiro, estamos usando window.setInterval para chamar nosso método draw repetidamente. Estamos tentando fazer isso 60 vezes por segundo (1000 milissegundos / 60,0).

window.setInterval(draw, 1000 / 60.0);

Em seguida, para cada vez que estamos desenhando, estamos determinando quanto tempo se passou desde o último quadro que foi desenhado e atualizando nosso progresso em direção à conclusão da animação. Quando o progresso chega à conclusão, invertemos a direção e a diminuímos de volta para 0. Então, quando atingimos 0, nos viramos e progredimos em direção a 1 novamente. Dessa forma, a animação apenas retrocede e avança continuamente.

time = new Date().getTime();
elapsed = time - lastTime;
lastTime = time;

if (forward) {
    progress += elapsed / duration;
} else {
    progress -= elapsed / duration;
}
if (progress > 1.0) {
    progress = 1.0;
    forward = false;
}
if (progress < 0) {
    progress = 0;
    forward = true;
}

Em seguida, o progresso é usado para direcionar a cor que estamos usando para renderizar a elipse.

b = 255.0 * progress;
context.fillStyle = "rgb(255," + Math.round(b.toString()) + ",0)";

E então temos uma elipse que anima em cores.

Melhorando nossa animação

Podemos fazer um pequeno ajuste no acima para melhorar a forma como a animação é executada. Existem duas suposições que a lógica acima faz que não são necessariamente verdadeiras. Ele pressupõe que a máquina que executa a lógica será rápida o suficiente para fornecer uma animação de 60fps e que o método de desenho será chamado com tempo confiável o suficiente para criar um efeito animado suave. Ambas as suposições não são necessariamente verdadeiras. A máquina pode ser muito lenta para executar a atualização de animação o número necessário de vezes por segundo, e setInterval não é garantido para ser chamado de forma confiável, especialmente se muita lógica estiver martelando a fila de eventos javascript. Alguns navegadores modernos suportam uma API que nos permite fazer melhor do que isso ao executar animação. Essencialmente, podemos solicitar que o navegador nos notifique quando um quadro de animação estiver pronto, para que possamos responder renderizando algum conteúdo. Isso fornece uma animação mais confiável e suave e deve evitar que tentemos animar mais quadros do que o sistema é capaz de suportar. Aqui está a versão modificada da lógica que usará um polyfill para usar a API requestAnimationFrame, se presente, e recorrerá ao nosso método anterior para animar se estivermos em um navegador que não dá suporte a requestAnimationFrame.

function ensureQueueFrame() {
    if (!window.queueFrame) {
        if (window.requestAnimationFrame) {
            window.queueFrame = window.requestAnimationFrame;
        } else if (window.webkitRequestAnimationFrame) {
            window.queueFrame = window.webkitRequestAnimationFrame;
        } else if (window.mozRequestAnimationFrame) {
            window.queueFrame = window.mozRequestAnimationFrame;
        } else {
            window.queueFrame = function (callback) {
                window.setTimeout(1000.0 / 60.0, callback);
            };
        }
    }
}

$(function () {
    var canv, context, lastTime, duration, progress, forward = true;
    ensureQueueFrame();

    $("#canvas").attr("width", "500px").attr("height", "500px");
    canv = $("#canvas")[0];
    context = canv.getContext("2d");

    lastTime = new Date().getTime();

    duration = 1000;
    progress = 0;

    function draw() {
        var ellapsed, time, b;
        queueFrame(draw);

        time = new Date().getTime();
        ellapsed = time - lastTime;
        lastTime = time;

        if (forward) {
            progress += ellapsed / duration;
        } else {
            progress -= ellapsed / duration;
        }
        if (progress > 1.0) {
            progress = 1.0;
            forward = false;
        }
        if (progress < 0) {
            progress = 0;
            forward = true;
        }

        b = 255.0 * progress;
        context.fillStyle = "rgb(255," + Math.round(b.toString()) + ",0)";
        context.beginPath();
        context.arc(150, 150, 120, 0, 2 * Math.PI, false);
        context.fill();
    }

    queueFrame(draw);
});

Da próxima vez

Da próxima vez, assumiremos algo muito mais ambicioso. Construiremos uma animação atraente que reage ao movimento do mouse sobre a tela. Também simularemos alguns efeitos 3D usando renderização 2D, mas transformações de matriz 3D.

Solicite uma demonstração