Pixelmancer
Generative art, Sandpiles fractal

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.

index.html
style.css
scripts.js
Copy

_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!

scripts.js
Copy

_10
const canvas = document.querySelector("canvas");
_10
const 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...

scripts.js
Copy

_10
const canvas = document.querySelector("canvas");
_10
const ctx = canvas.getContext("2d");
_10
ctx.fillStyle = "red";
_10
ctx.fillRect(0, 0, 100, 100);

E olha lá nosso quadrado vermelho! 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:

  1. Saber a posição dele em todos os momentos para que possamos aumentar ou diminuir esse valor
  2. 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.

scripts.js
Copy

_10
const canvas = document.querySelector("canvas");
_10
canvas.width = window.innerWidth;
_10
canvas.height = window.innerHeight;
_10
const ctx = canvas.getContext("2d");
_10
ctx.fillStyle = "red";
_10
ctx.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).

scripts.js
Copy

_17
const canvas = document.querySelector("canvas");
_17
canvas.width = window.innerWidth;
_17
canvas.height = window.innerHeight;
_17
const ctx = canvas.getContext("2d");
_17
ctx.fillStyle = "red";
_17
_17
let x = 100;
_17
let y = 100;
_17
_17
function animacao() {
_17
requestAnimationFrame(animacao);
_17
ctx.fillRect(x, y, 100, 100);
_17
x++;
_17
y++;
_17
}
_17
_17
animacao();

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: Quadrado vermelho
se movendo na diagonal 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.

scripts.js
Copy

_15
window.innerWidth; canvas.height = window.innerHeight; const ctx = canvas.getContext("2d");
_15
ctx.fillStyle = "red";
_15
_15
let x = 100;
_15
let y = 100;
_15
_15
function animacao() {
_15
requestAnimationFrame(animacao);
_15
ctx.clearRect(0,0,canvas.width, canvas.height)
_15
ctx.fillRect(x, y, 100, 100);
_15
x++;
_15
y++;
_15
}
_15
_15
animacao();

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.

scripts.js
Copy

_10
class 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.

scripts.js
Copy

_10
class 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
_10
const 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().

scripts.js
Copy

_14
class 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
_14
const 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:

scripts.js
Copy

_21
const canvas = document.querySelector("canvas");
_21
canvas.width = window.innerWidth;
_21
canvas.height = window.innerHeight;
_21
const ctx = canvas.getContext("2d");
_21
_21
class 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
_21
const quadradoVermelho = new Quadrado(0, 0, 100, "red");
_21
quadradoVermelho.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

scripts.js
Copy

_26
const canvas = document.querySelector("canvas");
_26
canvas.width = window.innerWidth;
_26
canvas.height = window.innerHeight;
_26
const ctx = canvas.getContext("2d");
_26
_26
class 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
_26
let quadrados = [];
_26
_26
for (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.

scripts.js
Copy

_35
const canvas = document.querySelector("canvas");
_35
canvas.width = window.innerWidth;
_35
canvas.height = window.innerHeight;
_35
const ctx = canvas.getContext("2d");
_35
_35
class 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
_35
let quadrados = [];
_35
_35
for (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
_35
for (let quadrado of quadrados) {
_35
quadrado.desenhar();
_35
}

Com isso, agora temos uma tela cheia de quadrados vermelhos espalhados aleatoriamento. 1000 quadrados vermelhos Se você estiver animado, você poderia criar quadrados de cores diferentes, ou talvez fazer eles se mexerem. Fica a cargo da sua imaginação!