6. Gerando animações

Para gerar animações por computador precisamos gerar novos desenhos, cada um pouco diferente do anterior, tipicamente a uma taxa de cerca de 30 quadros por segundo. Assim, conseguimos criar a ilusão de que os objetos da cena estão se movendo.

Imagine por exemplo um objeto se movendo no canvas com velocidade constante que, ao se chocar com uma das paredes, tem seu movimento refletido, como ilustrado abaixo:

Para gerar esse efeito, podemos calcular a nova posição do objeto a cada instante. Embora essa seja uma solução bem natural, o mesmo efeito pode ser alcançado movendo o canvas. Mas como assim, movendo o canvas!? A gente pode aplicar transformações geométricas sobre as coordenadas do canvas, usando o canvas como se fosse uma “janela” ou “câmera virtual” que podemos apontar para diferentes regiões do espaço. Nessa aula vamos explorar essas duas soluções.

6.1. Animação

Para nossa primeira solução, vamos gerar uma animação simplesmente redesenhando o objeto a cada novo quadro, em uma nova posição, atualizada aplicando uma velocidade horizontal velX e uma velocidade vertical velY. Toda vez que o objeto toca em uma borda do canvas (de dimensão (width, height)), sua velocidade (horizontal e/ou vertical) é refletida. A seguinte função redesenhe aplica essa atualização sobre a posição (posX, posY) do objeto.

//==================================================================
/**
* redesenha o canvas a cada intervalo
* @param {number} maxp
*/
function redesenhe( ) {
    // atualiza a cena, ou seja, a posição do objeto
    posX = posX + velX;
    posY = posY + velY;

    if (posX < 0) {
        velX *= -1;
        posX = -posX;
    }
    if (posX >= width) {
        velX *= -1;
        posX = width - (posX-width);
    }
    if (posY < 0) {
        velY *= -1;
        posY = -posY;
    }
    if (posY >= height) {
        velY *= -1;
        posY = height - (posY-height);
    }

    // ciclo da animação: limpa a tela e desenhe
    ctx.clearRect( 0, 0, width, height);
    // desenha um quadrado verde
    ctx.fillStyle = "green"
    desenheFillRect(posX-tam, posY-tam, tam*2, tam*2);

    // requisita o próximo redesenho
    window.requestAnimationFrame(redesenhe);
};

A função redesenhe é um exemplo típico de função para animação, constituída pelas seguintes etapas:

  • atualiza a cena, nesse caso recalculando a nova posição do objeto;
  • limpa o canvas usando ctx.clearRect(0, 0, width, height);
  • gera um novo desenho, atualizado; e
  • usa a função window.requestAnimationFrame(redesenhe) para chamar a mesma função redesenhe no próximo instante de “adequado”. Há outras formas gerar animações, por exemplo definindo explicitamente o intervalo de tempo para chamar a função de redesenho. Nesse curso vamos utilizar a função requestAnimationFrame, que procura gerar uma animação suave e eficiente, para gerar animações.

A janela abaixo contém o código desse exemplo, que você pode acessar no JSitor pelo link https://jsitor.com/t8JrrlH0v.

6.2. Transformações geométricas: translação

Nessa aula vamos apenas introduzir algumas transformações básicas e a ideia de composição de transformações geométricas. Vamos voltar a esse tópico em aulas futuras.

Para introduzir esses fundamentos, vamos por enquanto utilizar apenas 3 transformações básicas: translação, rotação e escala. A Figura Fig. 6.1 ilustra como uma translação podem ser aplicadas no canvas.

Efeito de uma translação do canvas

Fig. 6.1 Efeitos de uma translação do canvas. Fonte: Developer Mozilla.

Uma translação pode ser aplicada pela função translate(x,y). O resultado de uma translação por \((x, y)\) é como se a origem do canvas fosse movida para \((x, y)\).

6.2.1. Animação usando translações do canvas

A função translate aplica uma translação sobre canvas como um todo, ou seja, todos os desenhos após um translate(dx, dy) serão deslocados por (dx,dy). Lembre-se da nossa função redesenhe anterior que a atualização do objeto basicamente calcula sua nova posição. Ao invés de desenhar o objeto nessa nova posição, podemos criar uma função desenhaQuadrado que sempre desenha o quadrado ao redor da origem (ponto (0, 0)), como abaixo:

/**
* desenha um quadrado ao redor de (0, 0)
* @param {number} lado
*/
function desenheQuadrado(lado=40) {
    let l2 = Math.floor(lado/2);
    let x = -l2;
    let y = -l2;

    let quad = new Path2D();
    quad.moveTo( x, y);
    quad.lineTo( x+lado, y);
    quad.lineTo( x+lado, y+lado);
    quad.lineTo( x, y+lado);
    quad.closePath();
    ctx.fill(quad);
}

Observe que o canto superior esquerdo de um quadrado desenhado pela função desenheQuadrado sempre tem coordenadas negativas. Podemos usar a função translate para desenhar o quadrado ao redor de (posX, posY) modificando a função redesenhe como a seguir:

/**
* redesenha o canvas a cada intervalo
* @param {number} maxp
*/
function redesenhe( ) {
    // atualiza a cena, ou seja, a posição do objeto
    posX = posX + velX;
    posY = posY + velY;

    if (posX < 0) {
        velX *= -1;
        posX = -posX;
    }
    if (posX >= width) {
        velX *= -1;
        posX = width - (posX-width);
    }
    if (posY < 0) {
        velY *= -1;
        posY = -posY;
    }
    if (posY >= height) {
        velY *= -1;
        posY = height - (posY-height);
    }
    console.log(`Nova pos: (${posX}, ${posY}) `)

    // ciclo da animação: limpa a tela e desenhe
    ctx.clearRect( 0, 0, width, height);
    // desenha um quadrado verde
    ctx.fillStyle = "green"

    ctx.save();
    ctx.translate(posX, posY);
    desenheQuadrado(20);
    ctx.restore();

    // requisita o próximo redesenho
    requestAnimationFrame(redesenhe);
};

Observe que, a cada novo quadro, a nova translação deve ser aplicada sobre o canvas original e não sobre a última translação. A chamada ctx.save() armazena o estado do canvas antes de aplicar a translação e a chamada ctx.restore() restaura esse estado, evitando assim a composição de translações.

A janela abaixo contém o código do exemplo de animação, semelhante a anterior, mas agora usando translate, que você pode acessar no JSitor pelo link https://jsitor.com/1odDjrDD5.

6.3. Rotação e escala

Uma rotação pode ser aplicada pela função rotate( a ). Essa função aplica uma rotação por um ângulo \(\alpha\) (em radianos) sobre os eixos ao redor da origem, como ilustrado na Figura Fig. 6.2.

Efeito de uma rotação do canvas

Fig. 6.2 Efeitos de uma rotação do canvas. Fonte: Developer Mozilla.

Uma mudança de escala pode ser aplicada pela função scale(sx, sy). Os valores de \((sx, sy)\) podem ser reais menores que 1.0. Por exemplo, para diminuir o tamanho pela metade, podemos usar scale(0.5, 0.5). Já para aumentar o tamanho do desenho, por exemplo dobrar o tamanho, podemos usar scale(2.0, 2.0). Observe também que podemos esticar ou encolher o desenho arbitrariamente aplicando fatores de escala diferentes aos eixos horizontal e vertical ou, ainda, refletir o desenho usando valores negativos.

6.4. Composição de transformações

Imagine agora que temos uma função desenheQuadrado como abaixo, que desenha um quadrado de lado 50 centrado na origem.

/**
* desenha um quadrado ao redor de (0, 0)
* @param {number} lado
*/
function desenheQuadrado(lado=50) {
    let l2 = Math.floor(lado/2);
    let x = -l2;
    let y = -l2;

    let quad = new Path2D();
    quad.moveTo( x, y);
    quad.lineTo( x+lado, y);
    quad.lineTo( x+lado, y+lado);
    quad.lineTo( x, y+lado);
    quad.closePath();
    ctx.fill(quad);
}

Usando transformações de rotação, translação e escala, como podemos gerar a Figura Fig. 6.3 abaixo, formado por um quadrado azul de lado 100, rodado de \(45^o\), sob um quadrado vermelho de lado 50?

Desenho formado a partir de transformações do quadrado

Fig. 6.3 Exemplo de quadrados gerados a partir de transformações de translação, rotação e escala de um quadrado padrão.

Procure resolver esse problema no papel antes de prosseguir sua leitura.

Uma forma natural de pensar sobre a solução é começando pelo quadrado azul. Podemos rodar o quadrado de \(45^o\), ajustar a escala multiplicando por um fator de 2.0 e transladar o resultado até a posição desejada, no centro do canvas. O código poderia corresponder a algo como:

desenheQuadrado();
ctx.rotate(45.0 * Math.PI / 180.0 ); // 45o em radianos
ctx.scale(2.0, 2.0);
ctx.translate(width/2, height/2);

Embora natural, o resultado desse trecho não gera a figura desejada, exibida na Figura Fig. 6.3, devido a ordem das transformações estar “invertida” com relação ao canvas. Ou seja, o canvas precisa primeiro ser transladado, etc, para que o desenho seja gerado já sobre o canvas “transformado”. A função desenhe a seguir cria o efeito desejado.

function desenhe( ) {
    let w2 = Math.floor(width/2);
    let h2 = Math.floor(height/2);

    ctx.save();
    ctx.translate(w2, h2);  // translada para o centro do canvas
    ctx.rotate(45.0 * Math.PI / 180); // roda de 45
    ctx.scale(2.0, 2.0)  // escala * 2.0
    ctx.fillStyle = 'blue';
    desenheQuadrado();
    ctx.restore();

    ctx.save();
    ctx.translate(w2, h2);
    ctx.fillStyle = 'red';
    desenheQuadrado();
    ctx.restore();
};

A janela abaixo contém o código do exemplo de composição de transformações, que você pode acessar no JSitor pelo link https://jsitor.com/vB87meSWSG.

6.5. Onde estamos e para onde vamos?

Vimos que uma animação pode ser criada usando a função window.requestAnimationFrame para chamar a função responsável pelo redesenho do canvas. Tipicamente devemos limpar o canvas antes de redesenhar e atualizar os valores ou estado dos objetos que compõem o desenho antes de gerar o novo desenho. A última chamada da função de redesenho deve ser a resquestAnimationFrame.

Vimos também que dois tipos comuns para aplicar transformações aos desenhos. Uma é aplicando as transformações diretamente e outro por meio de composições de transformações primitivas como translação, rotação e escala. A composição é bastante utilizada pois nos permite simplificar as funções de desenho, considerando sempre a origem e uma orientação e escala padrão.

Os recursos de interação e animação que vimos até aqui são, embora típicos de outros sistemas gráficos, específicos do HTML e do canvas. A partir das próximas aulas vamos começar a tratar de outros fundamentos de programação gráfica, iniciando com uma visão da API WebGL.

6.6. Exercícios

  1. Modifique os exemplos de animação para incluir os seguintes controles:
    • tecla ‘d’ para incrementar a velocidade velX
    • tecla ‘a’ para decrementar a velocidade velX
    • tecla ‘w’ para incrementar a velocidade velY
    • tecla ‘s’ para decrementar a velocidade velY
    • slider para controlar o tamanho do objeto, que pode variar de 10 a 100 com passo 10.
  2. Modifique os exemplos de animação para incluir outros objetos, com outros tamanhos e cores.
  3. Modifique o exemplo de composição de transformações para fazer o quadrado azul rotacionar no sentido anti-horário com velocidade angular constante.
  4. A animação do quadrado foi simplificada e apresenta o seguinte artefato: é possível ver parte do quadrado “sumindo” atrás de algumas bordas do canvas. Aumente o tamanho do quadrado para que esse efeito seja mais notável. Altere o programa para que o quadrado seja refletido assim que um de seus lados toque uma borda do canvas.
  5. Crie um botão de Pausa que interrompe a animação ao ser clicado e que retoma a animação ao ser clicado novamente. Inclusive, altere o valor do botão para Execute, para reiniciar a animação.

6.7. Para saber mais