y
do
quadrado.O que é uma animação?
Uma animação é composta por alguma propriedade cujo valor se altera ao longo do tempo. Por exemplo, se um objeto está em uma posição em um momento e vai se movimentando em alguma direção, temos uma animação. Mas também se ele começa com uma cor e ela vai se alterando, também temos uma animação. Tipo estes dois exemplos:
Aqui ↑, a posição
x
do
quadrado começa em 0 e vai até 300 ao longo de um tempinho. Já
aqui ➚, a cor inicial do círculo é azul e a final é
amarela.
Basicamente, qualquer característica de um objeto que pode ser quantificada pode ter seu valor alterado ao longo do tempo, produzindo uma animação. Outro exemplo é o tamanho de um objeto.
A alteração dessas propriedades ocorre ao longo de um tempo definido para sua
duração.
Nestes exemplos, a
animação tem duração de 5s. Apesar de cada animação poder ter uma duração
diferente, é
bem
útil usar uma variável t
, que vai de 0 até 1 e indica a
porcentagem de
conclusão da animação -
independente da sua duração em segundos.
À direita podemos ver um círculo que, no tempo zero
(t = 0
) tem seu raio igual a 15 e, ao final
da animação (t = 1
) tem o raio igual a 45. Experimente você
mesmo definir o valor de
t
:
Mas como podemos implementar uma animação?
Implementando uma animação
Uma primeira ideia para fazer uma animação, digamos, da posição y
de um objeto, é criar
um
laço
for
fazendo o valor da variável ir de seu valor inicial 0 até o final
140. E digamos que queremos que a animação tenha duração de
5s.
const ctx = canvasEl.getContext('2d'); // linha 1
function desenhaQuadrado(x, y) { // linha 3
ctx.fillStyle = 'yellow';
ctx.fillRect(x, y, 30, 30);
}
for (let y = 0; y <= 140; y++) { // linha 9
desenhaQuadrado(10, y);
}
Ué, mas o que aconteceu ali ➚? Não houve animação... vamos pensar...
O laço for
das linhas 9-11 é
executado
141
vezes, muito rapidamente. Tão rápido quanto o navegador consegue, o que pode ser
algo
próximo de
instantaneamente. Vamos fazer um teste.
Nós vamos medir o tempo gasto para executar todo esse laço for
, marcando o tempo
antes e
o
tempo depois:
console.time(); // começa a contar
for (let y = 0; y <= 140; y++) {
desenhaQuadrado(10, y);
}
console.timeEnd(); // pára de contar
Poxa, mas isso foi bem rápido, não acha??
Acontece que o quadrado completou todo o seu movimento antes que pudéssemos ver - diferente do que queríamos, que acontecesse em 5s. Vamos analisar o que fizemos de certo, e o que precisamos mudar:
y
precisa ter um valor
inicial e
ir
incrementando.y
é alterado.for
não espera entre uma iteração e
outra,
mas
executa tão rápido quanto consegue.y
seja
alterado
de
0
a 140
ao longo de
5s
.
Então... precisamos esperar um tempinho para incrementar o valor de y
do
quadrado.
Portanto,
precisamos atualizar o valor da variável várias vezes, não necessariamente
141
vezes
apenas.
Para fazer alguma coisa várias vezes por segundo em JavaScript, podemos usar
setInterval(função, intervalo)
para registrar uma função
para
que
seja
chamada
a
cada intervalo
millissegundos. Vamos usar isso então e adaptar o código:
const ctx = canvasEl.getContext('2d');
function desenhaQuadrado(x, y) {
ctx.fillStyle = 'yellow';
ctx.fillRect(x, y, 30, 30);
}
const anim = {
horarioInicio: performance.now(), // início da animação
duracao: 5000, // tempo em millissegundos
inicial: 0, // y inicial do quadrado
final: 140, // y final do quadrado
t: 0 // porcentagem de conclusão
};
function atualiza() {
const horarioAgora = performance.now();
const tempoQueSePassou = horarioAgora - anim.horarioInicio;
// acha a porcentagem de conclusão da animação (de 0 a 1)
anim.t = tempoQueSePassou / anim.duracao;
if (anim.t > 1) {
// se chegamos ao final da animação (t = 100%), reiniciamos ela
anim.t = 0;
anim.horarioInicio = horarioAgora;
}
// define novo valor de y dada a porcentagem que já foi concluída
const y = anim.t * (anim.final - anim.inicial) + anim.inicial;
// desenha o quadrado na nova posição
desenhaQuadrado(10, y);
}
// registra função 'atualiza' para ser chamada a cada 33ms (30x/s)
setInterval(atualiza, 33);
Putz, que tanto de código! Mas vamos entender cada partinha. Nas linhas 8-14, criamos um objeto
anim
que
descreve a
animação que queremos fazer: um valor ir de 0
(anim.inicial
) a
140
(anim.final
) ao longo de 5.000ms (anim.duracao
). Esse objeto
também
contém
uma
propriedade t
que vai de 0
até 1
e representa a
porcentagem
de
conclusão da animação. E também o horarioInicio
que indica quando a animação
começou -
será
útil logo mais.
Já nas linhas 16-20, verificamos
quanto
tempo
se passou desde
o início da animação para podermos saber o quanto ela está concluída (0 a 100%), indicado
por
anim.t
.
Em 22-26 nós apenas verificamos se chegamos ao final e, se sim, voltamos a animação ao início.
Uma vez que já temos qual o novo valor de anim.t
, podemos achar qual é a posição
y
atual e,
em seguida, mandamos redesenhar o quadrado. Isso foi feito nas linhas 28-32.
Por fim, as linhas 34-35 registram a
função
atualiza
pra que ela seja chamada várias vezes por segundo - no caso, 30 vezes.
Com isso conseguimos implementar uma animação!!
Super legal! Funcionou mesmo. Mas o código ficou meio grandinho... Seria bem legal se a maior parte desse código ficasse mais escondida pra eu poder reaproveitá-lo para criar mais de uma animação!! Por exemplo, seria lindo se desse pra escrever o código tipo assim:
const ctx = canvasEl.getContext('2d');
function desenhaQuadrado(x, y) {
ctx.fillStyle = 'yellow';
ctx.fillRect(x, y, 30, 30);
}
// duração, valor inicial, valor final
const animacaoY = new Animacao(5000, 0, 140);
animacaoY.comecar();
function atualiza() {
animacaoY.atualizar(); const y = animacaoY.valor;
desenhaQuadrado(10, y);
}
setInterval(atualiza, 33);
Repare que as linhas 8-10,
13 e 14 criam ou usam um objeto do
tipo
Animacao
. Pra que possamos fazer isso, devemos ter uma classe com esse nome
e
com
os
métodos usados: comecar()
, atualizar()
e valor
.
Por um acaso, podemos escrever uma classe Animacao
desse
jeitinho,
basta
clicar ali pra ver!
Desse jeito aí de cima, escondendo o código referente a animações dentro de uma class
Animacao
, fica facinho fazer animações de mais de uma coisa, olha só:
const animacaoY = new Animacao(5000, 0, 140);
const animacaoX = new Animacao(5000, 30, 20);
const animacaoTamanho = new Animacao(5000, 30, 60);
function atualiza() {
animacaoX.atualizar();
animacaoY.atualizar();
animacaoTamanho.atualizar();
const x = animacaoX.valor;
const y = animacaoY.valor;
const tamanho = animacaoTamanho.valor;
desenhaQuadrado(x, y, tamanho);
}
animacaoX.comecar();
animacaoY.comecar();
animacaoTamanho.comecar();
setInterval(atualiza, 33);
Mas e se quisermos fazer animação além de posições, tamanhos e cores, mas com imagens também?
Animacao
um
pouco mais acima.
Usando imagens: sprites
Além de desenhar formas geométricas no canvas
, também é possível desenhar imagens. O
objeto
de contexto do canvas
possui o método drawImage(...)
, que tem o seguinte
formato:
ctx.drawImage(imagem, x, y);
Para definir qual imagem usar, podemos criar um objeto new Image(caminho)
e passá-la
para a
função ctx.drawImage(...)
. Tipo assim:
const ctx = canvasEl.getContext('2d');
const slime = new Image();
slime.src = 'imgs/slime.png';
// x, y
ctx.drawImage(slime, 10, 10);
Se quisermos desenhar o slime com um tamanho maior, por exemplo, também existe uma versão da função
ctx.drawImage(...)
que recebe 5 parâmetros:
const ctx = canvasEl.getContext('2d');
const slime = new Image();
slime.src = 'imgs/slime.png';
// larg, alt
ctx.drawImage(slime, 10, 10, 128, 128);
Como podemos ter várias propriedades que descrevem uma sprite, como sua posição, tamanho e imagem, podemos criar uma classe para representá-las.
class Sprite {
constructor(imagem, x, y, largura, altura) {
this.x = x;
this.y = y;
this.largura = largura;
this.altura = altura;
this.imagem = imagem;
}
desenhar(ctx) {
ctx.drawImage(
this.imagem,
this.x, this.y,
this.largura, this.altura);
}
}
const ctx = canvasEl.getContext('2d');
const imagem = new Image();
imagem.src = 'imgs/slime.png';
// exemplo de uso da classe
const slime = new Slime(imagem, 10, 10, 128, 128);
slime.desenhar(ctx);
Apesar de os códigos acima parecerem funcionar, eles têm um problema: se a imagem do slime não tiver
sido baixada previamente pelo navegador (e estiver em cache), ao executar
ctx.drawImage(slime...)
, podemos receber o seguinte erro:
Uncaught TypeError: Type error
Isso acontece porque a imagem começou a ser baixada na linha 3 e quando a linha 5 tentar desenhá-la, é muitíssimo provável que ela ainda não tenha sido baixada.
Para resolver esse problema, devemos aguardar o carregamento da imagem para então desenhá-la...
usando a classe Sprite
, podemos fazer assim:
class Sprite {
constructor(imagem, x, y, largura, altura) {
this.x = x;
this.y = y;
this.largura = largura;
this.altura = altura;
this.imagem = imagem;
this.imagem.addEventListener('load', () => this.imagemCarregada = true);
}
desenhar(ctx) {
if (this.imagemCarregada) {
ctx.drawImage(this.imagem, this.x, this.y, this.largura, this.altura);
}
}
}
Assim como fizemos quando animamos um quadrado, podemos animar a posição do slime.
Mas e se quisermos fazer uma animação como se fosse uma imagem .gif
?
Se mandarmos desenhar um arquivo .gif
contendo uma animaçãozinha em um
<canvas></canvas>
, apenas o primeiro quadro da animação será
mostrado, como se fosse um arquivo
.png
ou .jpg
.
Pra ter uma animação de imagens dentro de um canvas
, então, precisamos de uma imagem que
contenha todos os quadros de animação e usar uma técnica chamada animação com
spritesheets.
Revisão responda para ir adiante
Para passar para a próxima fase, responda: o que é uma Sprite?
Animação com spritesheets
Para fazermos uma animação de imagens em um <canvas></canvas>
, precisamos de
ter
uma mesma imagem contendo todos os quadros de animação (em inglês, uma
spritesheet). Olha essa imagem do
slime-atoa-spritesheet.png
.
![Quadros da animação do slime atoa](imgs/slime-atoa-spritesheet.png)
Tudo bem, mas o que fazemos com isso?!
A função ctx.drawImage(...)
tem uma terceira (e última) forma, que recebe 9 parâmetros.
Eles
são os seguintes:
ctx.drawImage(
imagem, // a imagem sendo desenhada
xQuadro, // o x do retângulo dentro da imagem
yQuadro, // o y do retângulo ...
larguraQuadro, // largura do quadro
alturaQuadro, // altura do quadro
x, // x do canvas onde vai aparecer
y, // y do canvas ...
largura, // largura no canvas
altura // altura no canvas
);
Repare que a imagem slime-atoa-spritesheet.png
tem o tamanho de 320x32 representando 10
quadros de animação ("subimagens"). Ou seja, se em 320 pixels de largura há 10 quadros,
cada quadro tem
tamanho 32x32.
Quando formos desenhá-la, vamos pegar apenas um desses quadros para desenhar. Se quisermos o primeiro, o segundo ou o último, faríamos respectivamente assim:
ctx.drawImage(
imagem,
0, // xQuadro
0, // yQuadro
32, // larguraQuadro
32, // alturaQuadro
x,
y,
largura,
altura
);
ctx.drawImage(
imagem,
32, // xQuadro
0, // yQuadro
32, // larguraQuadro
32, // alturaQuadro
x,
y,
largura,
altura
);
...
ctx.drawImage(
imagem,
320-32, // xQuadro
0, // yQuadro
32, // larguraQuadro
32, // alturaQuadro
x,
y,
largura,
altura
);
Nesse exemplo, para desenhar cada quadro, nós simplesmente precisamos encontrar qual o
xQuadro
para desenhar cada quadro. Como a largura (e altura) de cada quadro é 32, ficou
moleza!
function desenhaQuadro(quadro, x, y) {
ctx.drawImage(
slime, // imagem do slime
???, // xQuadro: uma fórmula
0, // yQuadro sempre 0
32, // larg/alt do quadro é 32
32,
x, // x,y do parâmetro
y,
32, // larg/alt sempre 32
32
);
}
Poderíamos inclusive criar uma função desenhaQuadro(quadro, x, y)
, que recebesse
um
número de
0
a 9
indicando qual quadro desenhar... como você a faria?
Muito bem! Agora, se juntarmos essa forma de desenhar apenas 1 quadro de uma imagem, com o que vimos
sobre animação, podemos alterar uma variável quadro
ao longo do tempo.
const animacao = new Animacao(800, 0, 9);
function desenhaQuadro(quadro, x, y) {
ctx.drawImage(
slime,
// origem na imagem
quadro * 32,
0,
32,
32,
// destino no canvas
x, y, 32, 32
);
}
function atualiza() {
animacao.atualizar();
let quadro = animacao.valor;
quadro = Math.floor(quadro);
desenhaQuadro(x, y, quadro);
}
animacao.comecar();
setInterval(atualiza, 33);
Na linha 1 criamos uma animação, que faz um valor ir de 0 a 9 em 0,8s.
E no loop de atualização, usamos esse valor como o quadro atual. Note nas linhas 18-19 que esse valor é
um
número real (pode ter parte fracionário, como 2.64, 3.61 etc.) e, portanto,
precisamos convertê-lo para inteiro, arrendondando para baixo com
Math.floor(numeroReal)
.
Show!! O resultado é ← este aqui. Com esse conhecimento, não é difícil imaginar como fazer uma animação com spritesheets se a imagem tiver os quadros não apenas em linha, mas em colunas também, como ⬋ esta aqui.
![](imgs/slime-spritesheet.png)
Com essa pequena alteração e a nova imagem, ficaria assim: experimente alterar a linha
let linha = 2;
function desenha(quadro, x, y) {
ctx.drawImage(
slime,
// origem na imagem
quadro * 32,
linha * 32,
32,
32,
// destino no canvas
x, y, 32, 32
);
}