O objetivo deste tutorial é explicar o elemento canvas e mostrar sua utilização fazendo algumas animações usando duas dimensões.
O que é o canvas?
É um elemento do HTML5 que pode ser usado para desenhar/renderizar gráficos, fazer composição de imagens e animações que normalmente são feitas por JavaScript.
E por que precisamos do canvas? Por que não usamos o próprio DOM do HTML?
Na verdade é possível fazer uma animação simples direto no DOM movendo elementos do HTML. A diferença é que o canvas é muito mais performático quando falamos em renderização. Se quisermos mover vários objetos usando 60fps, teremos um péssimo resultado fora do canvas que é otimizado para renderização.
É fácil usar o canvas porém, é preciso conhecimentos básicos de HTML5 e JavaScript antes de começar.
Vamos começar montando o esquelto da nossa aplicação:
-
Crie um diretório com o nome canvas;
-
Dentro desse diretório, crie 3 arquivos (index.html, app.css, app.js) com os respectivos códigos:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Canvas in the house!</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="app.css" />
</head>
<body>
<canvas id="my-canvas" width="500" height="500">
Your browser does not support canvas!
</canvas>
<script src="app.js"></script>
</body>
</html>
app.css
* {
margin: 0;
padding: 0;
}
body {
background-color: #cccccc;
text-align: center;
}
canvas {
margin-top: 50px;
background-color: black;
}
app.js
// Getting the DOM element.
const canvas = document.getElementById('my-canvas');
// Getting the 2d context.
const ctx = canvas.getContext('2d');
Obs. 1: por padrão, o tamanho do elemento canvas é de 300x150 pixels. Como é possível observar acima, configuramos nosso canvas para ser um quadrado com lateral de 500 pixels.
Obs. 2: caso o browser não tenha suporte ao canvas, o conteúdo que está dentro da tag será renderizado ao invés do container do canvas.
Obs. 3: o CSS tem apenas algumas configurações simples para vizualizarmos a área do nosso canvas.
Obs. 4: poderíamos ter colocado os códigos JavaScript que vamos utilizar para manipular o canvas dentro do próprio index.html mas, para deixarmos mais organizado, criamos o arquivo app.js.
- Antes de começarmos a codar, abra seu navegador, vá em Arquivo -> Abrir e navegue até a pasta deste tutorial. Selecione o arquivo index.html e marque para abrí-lo. Deverá aparecer uma página com fundo cinza e um quadrado preto desenhado.
Semelhante ao plano cartesiano, o canvas 2d tem dois eixos (x, y). Só tem uma pequena diferença em relação à orientação que você já está acostumado, que pode ser facilmente entendida na figura abaixo:
É isso, hora de codar! \o/
Agora sim...
Vamos desenhar um retângulo cinza no canto esquerdo superior do canvas. Para isso, substitua o código do arquivo app.js pelo código abaixo:
// Getting the DOM element.
const canvas = document.getElementById('my-canvas');
// Getting the 2d context.
const ctx = canvas.getContext('2d');
// Drawing the little rectangle.
ctx.fillStyle = 'grey';
ctx.fillRect(0, 0, 50, 50);
Vamos agora fazer esse retângulo se mover da esquerda para direita e parar. Para isso, basta arrumarmos alguma forma de incrementarmos a coordenada x do retângulo.
Se fizermos apenas um looping normal, o retângulo irá se mover mas, será muito rápido ou seja, precisamos de algum mecanismo que incremente o x com um intervalo de tempo que possamos ter algum controle de tempo para cada interação.
Existem 3 formas de fazer isso: setTimeout, setInterval e requestAnimationFrame.
-
setInterval(função, espera mínima) Chama a função que é passada como parâmetro repetidamente a cada intervalo especificado em milisegundos no segundo parâmetro
-
setTimeout(funcão, espera mínima) Chama a função uma vez após o tempo mínimo especificado no segundo parâmetro.
-
requestAnimationFrame(função de retorno) Fala para o browser controlar uma animação que será desenhada pela função passada como parâmetro.
Para este tutorial, vamos usar o requestAnimationFrame. Substitua novamente o código do app.js pelo código abaixo.
// Getting the DOM element.
const canvas = document.getElementById('my-canvas');
// Getting the 2d context.
const ctx = canvas.getContext('2d');
// Our animation control.
let running = false;
// Out X value that will be incremented.
let rectX = 0;
// Stops the animation.
const stopAnimation = () => running = false;
// Function that draws the rectangle.
const drawRectangle = (x) => {
// Stops the animation when the rectangle reaches the end.
if (x > canvas.width - 50) {
stopAnimation();
}
ctx.fillStyle = 'grey';
ctx.fillRect(x, 0, 50, 50);
};
// Each loop we call render function.
const render = () => {
// We clean everything in the canvas.
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Call the function that draws the rectangle incrementing x.
drawRectangle(rectX += 1);
// Calling the animation again and again.
if (running) {
window.requestAnimationFrame(render);
}
}
// Starting animation.
running = true;
render();
Como você pode ver, implementamos um looper que chama a função render a cada 10 milisegundos.
A função render cada vez que é chamada limpa o canvas e chama a função drawRectangle incrementando a coordenada x do retângulo em 1 pixel.
Até agora nosso código está bem simples. Vamos fazer algumas alterações para conseguirmos reaproveitar objetos, desenhar vários no canvas e animá-los.
Nesta etapa vamos usar um pouco de POO (programação orientada à objetos).
O primeiro passo é fazer uma classe que nos permitirá ter várias instâncias de objetos. Cada instância terá suas próprias propriedades.
A ideia é que a cada ciclo de renderização mudemos os parâmetros de cada instância para que todas se movimentem.
Aqui já começamos a ver a performance do canvas ao renderizar vários objetos simultaneamente.
// Some app setup.
const ENEMIES_STORE = [];
const ENEMIES_SIZE = 50;
const ENEMIES_COLORS = [
'red', 'blue', 'yellow', 'white', 'grey',
'green', 'purple', 'navy', 'silver', 'olive',
'lime', 'fuchsia', 'teal', 'aqua', 'maroon'
];
// Getting the DOM element.
const canvas = document.getElementById('my-canvas');
// Getting the 2d context.
const ctx = canvas.getContext('2d');
// Our animation control.
let running = false;
// Animation frames counter.
let frames = 0;
// Our class that will generate enemies instances.
class Enemy {
constructor(x) {
this.x = x;
this.y = 0;
this.width = ENEMIES_SIZE;
this.height = ENEMIES_SIZE;
// Getting a random color when the object is instantiated.
this.color = ENEMIES_COLORS[Math.floor(Math.random() * ENEMIES_COLORS.length)];
}
draw() {
this.y += 10;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, ENEMIES_SIZE, ENEMIES_SIZE);
}
}
// This function just instantiate one enemy in a random x position and add it to the array of enemies.
const createEnemy = () => {
// Each 50 frames we create a new enemy.
if (frames % 5 === 0) {
// Set enemy x coordinate from 0 to 450.
const x = Math.floor(Math.random() * 10) * ENEMIES_SIZE;
// Adding the enemy to the array of enemies.
ENEMIES_STORE.push(new Enemy(x));
}
}
// This functions performs a loop in the enemies array and draw each enemy.
const drawEnemies = () => {
// Drawing all enemies.
ENEMIES_STORE.forEach(enemy => enemy.draw());
}
// Canvas cleaner.
const resetCanvas = () => ctx.clearRect(0, 0, canvas.width, canvas.height);
// Each loop we call render function.
const render = () => {
// We clean everything in the canvas.
resetCanvas();
// Incremeting frames for each loop.
frames += 1;
// Intantiate one new enemy at a random x position and add it to the enemies array.
createEnemy();
// Draw all enemies available in the enemies array.
drawEnemies();
// Calling the animation again and again.
if (running) {
window.requestAnimationFrame(render);
}
}
// Starting animation.
running = true;
render();
Agora no seu canvas tem uma "chuva" de retângulos com cores aleatórias.
Para essa implementação fizemos uma classe de inimigos (Enemies). A cada chamada da nossa função render, um novo inimigo é criado.
Assim que criamos um inimigo, o colocamos na estrutura que tem todos os inimigos criados. Depois disso, chamamos a função que desenha todos os inimigos existentes no canvas.
Essa abordagem tem um problema grave pois, a cada passagem pela função de renderização fazemos um looping em todos os inimigos incluíndo aqueles que já passaram pelo canvas. Isso causará degradação de performance e alto consumo de memória e processamento.
Por enquanto continuaremos assim! :-(
Já que temos inimigos vindo de cima para baixo, que tal criarmos um herói que seja controlado pelo teclado e que tenha que - por enquanto - fugir dos inimigos?
// Some app setup.
const ENEMIES_STORE = [];
const ENEMIES_SIZE = 50;
const ENEMIES_COLORS = [
'red', 'blue', 'yellow', 'white',
'green', 'purple', 'navy', 'silver', 'olive',
'lime', 'fuchsia', 'teal', 'aqua', 'maroon'
];
const HERO_SIZE = ENEMIES_SIZE;
const HERO_COLOR = 'grey';
// Getting the DOM element.
const canvas = document.getElementById('my-canvas');
// Getting the 2d context.
const ctx = canvas.getContext('2d');
// Our animation control.
let running = false;
// Animation frames counter.
let frames = 0;
// Our class that will generate enemies instances.
class Enemy {
constructor(x) {
this.x = x;
this.y = 0;
this.width = ENEMIES_SIZE;
this.height = ENEMIES_SIZE;
// Getting a random color when the object is instantiated.
this.color = ENEMIES_COLORS[Math.floor(Math.random() * ENEMIES_COLORS.length)];
}
draw() {
this.y += 10;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, ENEMIES_SIZE, ENEMIES_SIZE);
}
}
// Our Hero class.
class Hero {
constructor() {
this.x = 0;
this.y = canvas.height - HERO_SIZE;
this.width = HERO_SIZE;
this.height = HERO_SIZE;
this.color = HERO_COLOR;
}
draw() {
// Prevent our hero from going beyond the available area.
if (this.x < 0) this.x = 0;
if (this.x > canvas.width - HERO_SIZE) this.x = canvas.width - HERO_SIZE;
// Drawing the hero itself.
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, HERO_SIZE, HERO_SIZE);
}
}
// This function just instantiate one enemy in a random x position and add it to the array of enemies.
const createEnemy = () => {
// Each 50 frames we create a new enemy.
if (frames % 5 === 0) {
// Set enemy x coordinate from 0 to 450.
const x = Math.floor(Math.random() * 10) * ENEMIES_SIZE;
// Adding the enemy to the array of enemies.
ENEMIES_STORE.push(new Enemy(x));
}
}
// This functions performs a loop in the enemies array and draw each enemy.
const drawEnemies = () => {
// Drawing all enemies.
ENEMIES_STORE.forEach(enemy => enemy.draw());
}
// We just need one hero, so let's instantiate it.
const ourHero = new Hero();
// Canvas cleaner.
const resetCanvas = () => ctx.clearRect(0, 0, canvas.width, canvas.height);
// Each loop we call render function.
const render = () => {
// We clean everything in the canvas.
resetCanvas();
// Incremeting frames for each loop.
frames += 1;
// Drawing our hero.
ourHero.draw();
// Intantiate one new enemy at a random x position and add it to the enemies array.
createEnemy();
// Draw all enemies available in the enemies array.
drawEnemies();
// Calling the animation again and again.
if (running) {
window.requestAnimationFrame(render);
}
}
// Starting animation.
running = true;
render();
// Keyboard listener to check if the user press arrows keys.
window.addEventListener('keydown', (e) => {
// Left arrow key.
if (e.keyCode === 37) {
if (ourHero.x <= 0) return;
ourHero.x -= HERO_SIZE;
}
// Right arrow key.
if (e.keyCode === 39) {
if (ourHero.x >= canvas.width - HERO_SIZE) return;
ourHero.x += HERO_SIZE;
}
});
Como você pode ver no código acima, agora também temos uma classe que cria nosso herói. O herói será renderizado a cada chamada da nossa função render com a diferença que apenas a posição dele será atualizada. Não teremos criação de novos heróis.
Adicionamos um listener que dispara uma função toda vez que alguma tecla é pressionada (keydown). Capturamos esse evento e verificamos se o código da tecla pressionada equivale a seta esquerda ou direita.
Caso seja uma das setas, mudamos a posição do nosso herói.
Agora já temos quase um jogo funcional. Falta apenas implementarmos um detector de colisões e uma mensagem de Game Over!
// Some app setup.
const ENEMIES_STORE = [];
const ENEMIES_SIZE = 50;
const ENEMIES_COLORS = [
'red', 'blue', 'yellow', 'white',
'green', 'purple', 'navy', 'silver', 'olive',
'lime', 'fuchsia', 'teal', 'aqua', 'maroon'
];
const HERO_SIZE = ENEMIES_SIZE;
const HERO_COLOR = 'grey';
const GAME_OVER_FONT = '20px Verdana';
const GAME_OVER_COLOR = 'white';
const GAME_OVER_TEXT = 'GAME OVER';
const GAME_OVER_X = 185;
const GAME_OVER_Y = 220;
// Getting the DOM element.
const canvas = document.getElementById('my-canvas');
// Getting the 2d context.
const ctx = canvas.getContext('2d');
// Our animation control.
let running = false;
// Animation frames counter.
let frames = 0;
// Our class that will generate enemies instances.
class Enemy {
constructor(x) {
this.x = x;
this.y = 0;
this.width = ENEMIES_SIZE;
this.height = ENEMIES_SIZE;
// Getting a random color when the object is instantiated.
this.color = ENEMIES_COLORS[Math.floor(Math.random() * ENEMIES_COLORS.length)];
}
draw() {
this.y += 10;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, ENEMIES_SIZE, ENEMIES_SIZE);
}
}
// This function just instantiate one enemy in a random x position and add it to the array of enemies.
const createEnemy = () => {
// Each 50 frames we create a new enemy.
if (frames % 5 === 0) {
// Set enemy x coordinate from 0 to 450.
const x = Math.floor(Math.random() * 10) * ENEMIES_SIZE;
// Adding the enemy to the array of enemies.
ENEMIES_STORE.push(new Enemy(x));
}
}
// This functions performs a loop in the enemies array and draw each enemy.
const drawEnemies = () => {
// Drawing all enemies.
ENEMIES_STORE.forEach(enemy => enemy.draw());
}
// Colission checker.
const collisionChecker = () => {
ENEMIES_STORE.forEach(enemy => {
if (ourHero.checkCollision(enemy)) {
gameOver();
}
})
}
// Our Hero class.
class Hero {
constructor() {
this.x = 0;
this.y = canvas.height - HERO_SIZE;
this.width = HERO_SIZE;
this.height = HERO_SIZE;
this.color = HERO_COLOR;
}
draw() {
// Prevent our hero from going beyond the available area.
if (this.x < 0) this.x = 0;
if (this.x > canvas.width - HERO_SIZE) this.x = canvas.width - HERO_SIZE;
// Drawing the hero itself.
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, HERO_SIZE, HERO_SIZE);
}
// Checks the hero position against enemies position.
checkCollision(enemy) {
return (this.x < enemy.x + enemy.width) && (this.x + this.width > enemy.x) && (this.y < enemy.y + enemy.height) && (this.y + this.height > enemy.y);
}
}
// We just need one hero, so let's instantiate it.
const ourHero = new Hero();
// Canvas cleaner.
const resetCanvas = () => ctx.clearRect(0, 0, canvas.width, canvas.height);
// Stop the looper and print game over message.
const gameOver = () => {
running = false;
ctx.font = GAME_OVER_FONT;
ctx.fillStyle = GAME_OVER_COLOR;
ctx.fillText(GAME_OVER_TEXT, GAME_OVER_X, GAME_OVER_Y);
}
// Each loop we call render function.
const render = () => {
// We clean everything in the canvas.
resetCanvas();
// Incremeting frames for each loop.
frames += 1;
// Drawing our hero in the current position.
ourHero.draw();
// Instantiate one new enemy at a random x position and add it to the enemies array.
createEnemy();
// Draw all enemies available in the enemies array.
drawEnemies();
// Collision checker.
collisionChecker();
// Calling the animation again and again.
if (running) {
window.requestAnimationFrame(render);
}
}
// Starting animation.
running = true;
render();
// Keyboard listener to check if the user press arrows keys.
window.addEventListener('keydown', (e) => {
// Left arrow key.
if (e.keyCode === 37) {
if (ourHero.x <= 0) return;
ourHero.x -= HERO_SIZE;
}
// Right arrow key.
if (e.keyCode === 39) {
if (ourHero.x >= canvas.width - HERO_SIZE) return;
ourHero.x += HERO_SIZE;
}
});
Criamos nesta última etapa a função gameOver, a função collisionChecker e o método no nosso herói chamado checkCollision.
E temos um jogo funcionando! \o/
Desafios
- Acertar o leak de inimigos!
- Implementar pause/play no jogo usando a barra de espaço.
- Alterar todo o cenário para termos um fundo, um herói e inimigos!
- Montar um sistema de score.
- Fazer nosso herói atirar.
- Matar inimigos.
- Alterar sistema de score.
- Sonorizar.
Rodrigo Leme de Mello @lemerodrigo
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial