Written by - Hugo Billé Martins
O que é possível fazer?
Quando falamos de "desenhos" automaticamente lembramos de um papel
e uma caneta; nesse caso também não é muito diferente, usamos funções
como fillRect()
e arc()
para desenhar formas geométricas básicas que,
em conjunto, podem formar qualquer desenho.
Porém, é óbvio que esses tipos de desenhos não são exatamente "humanos", costumam ser simétricos e "muito perfeitos", características de desenhos feitos usando código (também conhecido como "Generative Art").
Um exemplo desses desenhos é o banner dessa publicação, que é um fractal feito utilizando o Canvas API, definitivamente muito perfeito para ser feito por uma pessoa. Se tiver interesse, recomendo acessar minha §galeria com todos os meus projetos utilizando o canvas.
O Canvas API permite de maneira muito fácil criar desenhos tanto em 2D quanto em 3D. Esse tutorial pretende apenas introduzir como utilizar essa ferramenta tão útil em 2D, e claro, utilizando conceitos de OOP.
Estrutura inicial
Para começar a trabalhar com o canvas, precisamos antes criar um elemento
<canvas>
no nosso html. Também é importante já criarmos algumas regras no nosso css
para tirar o margim e o padding padrão dos naveagdores, o famoso reset.
_17<!DOCTYPE html>_17<html lang="pt-br">_17 <head>_17 <meta charset="UTF-8" />_17 <meta name="viewport" content="width=device-width, initial-scale=1.0" />_17 <link rel="stylesheet" href="style.css" />_17 <title>Lorem Ipsum</title>_17 </head>_17_17 <body>_17 <main>_17 <canvas id="canvas"></canvas>_17 </main>_17 </body>_17_17 <script src="./scripts.js"></script>_17</html>
Criando o contexto
Agora sim podemos começar! O primeiro passo para usarmos
o Canvas API é pegar pelo DOM o elemento <canvas>
que criamos
no HTML. Podemos fazer isso com o querySelector('canvas')
ou getElementByID('canvas')
,
as duas maneiras irão funcionar. Com o elemento em mãos, podemos chamar o método getContext('2d')
,
que criará um objeto CanvasRenderingContext2D, que contém todas as funções que precisamos para
desenhar no nosso canvas. lembrando que também existe o Context3D, que usamos
para desenhar em... 3D!
_10const canvas = document.querySelector("canvas");_10const ctx = canvas.getContext("2d");
Desenhando pela primeira vez
Agora que temos nosso ctx, podemos finalmente começar a desenhar!
Para desenhar um quadro, utilizamos o método fillRect()
do ctx; essa função
recebe 4 valores, os dois primeiros são as coordenadas x, y e os outros dois são
a altura e largura do quadrado. Nesse primeiro exemplo vou desenhar um quadro com lado 100
na posição 0,0.
Mas pera ai, ainda não escolhemos a cor do quadrado! Estamos falando para
o ctx criar um quadrado mas nem falamos com que "tinta". Para isso, colocamos alguma cor
na propriedade fillStyle
do ctx. Pode ser rgb, rgba, hsl, hex ou mesmo o nome quinem no css...
_10const canvas = document.querySelector("canvas");_10const ctx = canvas.getContext("2d");_10ctx.fillStyle = "red";_10ctx.fillRect(0, 0, 100, 100);
E olha lá nosso quadrado vermelho!
Onde que é esse tal 0,0?
Diferente de um plano cartesiano, o ponto de origem do canvas, por padrão (podemos mudar isso depois), é o quando superior esquerdo da tela. Por isso que o quadrado está bem no cantinho esquerdo do nosso canvas.
Movendo o quadrado
Para mover esse quadrado precisamos de duas coisas:
- Saber a posição dele em todos os momentos para que possamos aumentar ou diminuir esse valor
- Criar um loop infinito para criar a animação
O primeiro problema é fácil de resolver, só precisamos criar uma variável x e y para guardar a posição do quadrado. Esse "loop infinito" também é fácil de resolver, vamos ver isso logo já. Primeiro, precisamos fazer o canvas preencher a tela inteira para que haja mais espaço para desenhar.
Canvas preenchendo toda a tela
O jeito mais fácil de fazer isso é utilizando as propriedades
width e height do canvas. Vamos definir que seu tamanho seja igual
ao window.innerWidth
e window.innerHeight
.
_10const canvas = document.querySelector("canvas");_10canvas.width = window.innerWidth;_10canvas.height = window.innerHeight;_10const ctx = canvas.getContext("2d");_10ctx.fillStyle = "red";_10ctx.fillRect(0, 0, 100, 100);
Atualizando a posição
Vamos criar uma variável x e y para guardar a posição do quadrado
e aumentar esses valores a cada frame da animação. Para criar uma animação
utilizados a função requestAnimationFrame()
, que vai executar uma função em
recursão mas respeitando o limite de 60 frames por segundo ou mais, dependendo do seu monitor e poder de processamento do computador.
Meio difícil de entender, certo? Vamos colocar isso em prática. Vamos criar uma função
animacao()
e dentro dela vamos chamar requestAnimationFrame(animacao)
.
_17const canvas = document.querySelector("canvas");_17canvas.width = window.innerWidth;_17canvas.height = window.innerHeight;_17const ctx = canvas.getContext("2d");_17ctx.fillStyle = "red";_17_17let x = 100;_17let y = 100;_17_17function animacao() {_17 requestAnimationFrame(animacao);_17 ctx.fillRect(x, y, 100, 100);_17 x++;_17 y++;_17}_17_17animacao();
O que está acontecendo aqui é o seguinte: chamamos animacao()
pela primeira vez na linha 17, e devido ao requestAnimationFrame(animacao)
, essa função será executada em todo o frame da aniação, se possível. Nesse caso, a função animacao()
está desenhando o quadrado e logo depois adicionando 1 em x e y.
Se você tentar executar esse código, você vai obter algo assim: Resultado
meio estranho... O que está acontecendo? Basicamente, estamos desenhando todo o frama
da animação mas não estamos limpando a tinta dos desenhos. Para resolver isso, precisamos
usar o método clearRect()
do ctx. Ele recebe 4 argumentos: x e y inicial e x
e y final, formando uma área que será limpa. No nosso caso, queremos limpar a tela
toda então passamos 0,0 como valores iniciais e canvas.width e canvas.height como
valores finais. Vale lembrar que precisamos limpar o canvas antes de todos os frames.
_15window.innerWidth; canvas.height = window.innerHeight; const ctx = canvas.getContext("2d");_15ctx.fillStyle = "red";_15_15let x = 100;_15let y = 100;_15_15function animacao() {_15requestAnimationFrame(animacao);_15ctx.clearRect(0,0,canvas.width, canvas.height)_15ctx.fillRect(x, y, 100, 100);_15x++;_15y++;_15}_15_15animacao();
Abstração com OOP
Por enquanto foi tudo bem fácil, desenhar um único quadrado que se move na diagonal foi uma tarefa bem rápida. Mas e se, ao invés de um único quadrado, fossem 1000? Seria bem inconveniente criar 1000 variáveis x e y. A forma que vamos solucionar esse problema é utilizando classes que, além de deixar bem claro o que cada função faz, permite a gente criar inúmeros quadrados de maneira bem fácil.
Vamos começar criando uma classe Quadrado (é costume que nome de classes comecem com letra maiúscula). Essa classe vai armazenar 4 valores por enquanto: coordenadas x e y, tamanho e cor. Toda classe tem disponível a função constructor()
, que é iniciada assim que criamos uma instância (não se preocupe com essa palavra, já vamos chegar nela) dessa classe.
Precisamos guardar os valores que recebemos no constructor para que possamos utiliza-los fora dessa função e modifica-los. Para isso, usamos a keyword this
, que de modo bem simples se refere ao espaço que estamos no momento; ao usar o this dentro da classe Quadrado, dizemos que queremos definir as propriedade do Quadrado.
_10class Quadrado {_10 constructor(x, y, tamanho, cor){_10 this.x = x;_10 this.y = y;_10 this.tamanho = tamanho;_10 this.cor = cor;_10 }_10}
Criando uma instância
Ok, agora que temos nossa classe, podemos criar uma instância dela, que é simplesmente uma cópia. Podemos criar x instância de uma classe com a keyword new
. Vamos criar uma variável
"quadradoVermelho" e salvar nela uma instância de Quadrado.
_10class Quadrado {_10 constructor(x, y, tamanho, cor) {_10 this.x = x;_10 this.y = y;_10 this.tamanho = tamanho;_10 this.cor = cor;_10 }_10}_10_10const quadradoVermelho = new Quadrado(0, 0, 100, "red");
Pronto! criamos uma instância do quadradoVermelho. Por enquanto essa classe é meio inútil porque ela não tem nenhum método, mas sua estrutura já está pronta.
O que seria muito útil para a gente agora seria criar uma método
desenhar()
para a classe Quadrado. Sempre que quisermos desenhar o
quadradoVermelho
podemos simplesmente fazer quadradoVermelho.desenhar()
.
_14class Quadrado {_14 constructor(x, y, tamanho, cor) {_14 this.x = x;_14 this.y = y;_14 this.tamanho = tamanho;_14 this.cor = cor;_14 }_14 desenhar() {_14 ctx.fillStyle = this.cor;_14 ctx.fillRect(this.x, this.y, this.tamanho, this.tamanho);_14 }_14}_14_14const quadradoVermelho = new Quadrado(0, 0, 100, "red");
Sempre que for necessário acessar as propriedades do Quadrado, podemos utilizar o this
.
Agora, utilizando o que aprendemos para fazer a mesma coisa de antes, teríamos:
_21const canvas = document.querySelector("canvas");_21canvas.width = window.innerWidth;_21canvas.height = window.innerHeight;_21const ctx = canvas.getContext("2d");_21_21class Quadrado {_21 constructor(x, y, tamanho, cor) {_21 this.x = x;_21 this.y = y;_21 this.tamanho = tamanho;_21 this.cor = cor;_21 }_21_21 desenhar() {_21 ctx.fillStyle = this.cor;_21 ctx.fillRect(this.x, this.y, this.tamanho, this.tamanho);_21 }_21}_21_21const quadradoVermelho = new Quadrado(0, 0, 100, "red");_21quadradoVermelho.desenhar();
Tivemos mais trabalho para começar a desenhar mas agora, se quisermos desenhar milhares de quadrados, ficou infinitamente mais fácil. Além disso, ficou mais fácil de adicionar novas funcionalidades para o Quadrado; se você quiser adicionar a funcionalidade de "mover" para o quadradoVermelho, é só você ir criar um novo método que faça isso. O objetivo do OOP é facilitar criar novas funcionalidade e deixar mais simples para que outras pessoas utilizam essas funções: não fica super fácil de entender o que quadradoVermelho.desenhar()
faz?
Desenhando 1000 quadrados
Para começar essa empreitada, precisamos criar um vetor que vai armazenar todas essas instâncias. Vamos chamar esse vetor de quadrados e ir empurrando quadrados em posições aleatória para esse vetor
_26const canvas = document.querySelector("canvas");_26canvas.width = window.innerWidth;_26canvas.height = window.innerHeight;_26const ctx = canvas.getContext("2d");_26_26class Quadrado {_26 constructor(x, y, tamanho, cor) {_26 this.x = x;_26 this.y = y;_26 this.tamanho = tamanho;_26 this.cor = cor;_26 }_26_26 desenhar() {_26 ctx.fillStyle = this.cor;_26 ctx.fillRect(this.x, this.y, this.tamanho, this.tamanho);_26 }_26}_26_26let quadrados = [];_26_26for (let i = 0; i < 1000; i++) {_26 quadrados.push(_26 new Quadrado(Math.random() * 100, Math.random() * 100, 10, "red")_26 );_26}
Agora que temos esse vetor, podemos iterar por ele com um Array.forEach()
, um loop normal ou um for loop. Fica a seu critério qual usar, o importante é percorrer cada instância salva no vetor quadrados e chamar a função desenhar.
_35const canvas = document.querySelector("canvas");_35canvas.width = window.innerWidth;_35canvas.height = window.innerHeight;_35const ctx = canvas.getContext("2d");_35_35class Quadrado {_35 constructor(x, y, tamanho, cor) {_35 this.x = x;_35 this.y = y;_35 this.tamanho = tamanho;_35 this.cor = cor;_35 }_35_35 desenhar() {_35 ctx.fillStyle = this.cor;_35 ctx.fillRect(this.x, this.y, this.tamanho, this.tamanho);_35 }_35}_35_35let quadrados = [];_35_35for (let i = 0; i < 1000; i++) {_35 quadrados.push(_35 new Quadrado(_35 Math.random() * canvas.width,_35 Math.random() * canvas.height,_35 10,_35 "red"_35 )_35 );_35}_35_35for (let quadrado of quadrados) {_35 quadrado.desenhar();_35}
Com isso, agora temos uma tela cheia de quadrados vermelhos espalhados aleatoriamento. Se você estiver animado, você poderia criar quadrados de cores diferentes, ou talvez fazer eles se mexerem. Fica a cargo da sua imaginação!