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. 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 “camera virtual” que podemos apontar para diferentes regiões do espaço. Nessa aula vamos explorar essas duas soluções.
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:
ctx.clearRect(0, 0, width, height)
;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.
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.
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)\).
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.
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.
Fig. 6.2 Efeitos de uma rotação do canvas. Fonte: Developer Mozilla.¶
Uma mudança de escala por 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.
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?
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.
Vimos que a animação pode ser criada usando função window.requestAnimationFrame
para chamar a função responsável pelo redesenho do canvas. Tipicamente essa funçãod deve limpar o canvas antes de redesenhar, atualizar os valores para 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 tipicos 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.
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 reniciar a animação.