4. Desenhando no Canvas HTML com JavaScript

Vamos usar o canvas para gerar desenhos e animações 2D e 3D em uma página web, usando HTML e JavaScript.

Nessa aula vamos introduzir as ferramentas de programação que vamos utilizar para resolver os exercícios. Lembre-se que, embora essas ferramentas sejam muito relevantes para essa disciplina, elas não fazem parte do foco principal. Por isso, nessa aula vamos apenas introduzi-las por meio de exemplos ilustrando alguns recursos básicos. Você pode copiar e depois estender esses exemplos para realizar os exercícios.

4.1. Esqueleto HTML para usar o canvas

Vamos começar apresentando um esqueleto HTML que cria um canvas (área de desenho) para começar a desenhar em 2D, usando algumas primitivas gráficas comuns do HTML5.

A sigla HTML significa HyperText Markup Language, ou seja, é uma linguagem que permite criar hipertexto como links URL e outros elementos como o próprio canvas usando tags. A versão HTML5 introduziu várias recursos de multimídia inclusive o elemento Canvas que cria uma área de desenho.

A tag <!DOCTYPE html> na primeira linha do arquivo indica que se trata de um arquivo HTML5. O documento HTML abre e fecha usando as tags <html> e </html>. A parte delimitada por <head> e </head> não é exibida pelo navegador quando a página é carregada. Ela tipicamente contém:

  • um título delimitado pelas tags <title> </title>;
  • links para os arquivos CSS usados para configurar os estilos usados pelos elementos da página, com extensão .css;
  • e outros meta-dados, alguns com a tag <meta> e, no nosso caso, deve conter também o arquivo que contém os programas em JavaScript, que tem extensão .js. Nesse exemplo vamos usar o arquivo script.js.

O trecho delimitado por <body> </body> é a parte que será exibida pelo navegador. Para criar um canvas basta copiar o trecho de código HTML abaixo para um arquivo canvas.html. Observe que no corpo (<body>) precisamos incluir uma tag <canvas> </canvas>, para criar uma área de desenho. Dentro de cada tag você pode definir algumas opções para configurar o elemento HTML. No caso da tag <canvas>, o canvas.html cria um elemento canvas com id=meucanvas e tamanho \(640 \times 480\) (width \(\times\) height) pixels em um sistema de coordenadas com o eixo x para a direita e y para baixo como ilustrado a seguir:

Coordenadas no Canvas

Fig. 4.1 Coordenadas no Canvas (reproduzido de Tutorial do Canvas)

Infelizmente, nem todos os navegadores são compatíveis com o canvas. Para esses navegadores, podemos incluir uma mensagem alternativa entre os tags <canvas> </canvas>, como por exemplo a mensagem “Opa! pode ser que seu navegador não suporte o canvas do HTML5.”. Assim, esses navegadores, ao invés do canvas, exibem a mensagem alternativa.

No exemplo incluímos também outros tags HTML para você se divertir ainda mais criando listas usando <ul> e <li>, sessões usando <h1> e <h2> etc.

4.1.1. Exemplo de um esqueleto HTML usando o canvas

Listing 4.1 arquivo canvas.html
<!DOCTYPE html>
<html lang="pt-br">
  <head>
    <meta charset="utf-8">
    <title>Meu canvas</title>
    <link rel="stylesheet" href="./estilo.css" type="text/css">
    <script src="meu_script.js" defer></script>
  </head>

  <body>
    <!--  comentário em html -->
    <h1>Meu primeiro canvas</h1>
    <p>Um parágrafo</p>
 
    <!-- lista -->
    <ul>
        <li> item um</li>
        <li> item dois</li>
        <li> outro item dois</li>
    </ul>

    <hr>
    <!-- 
        IMPORTANTE 
        O CANVAS 
        ;-) 
        -->
    <h2>Esse é o CANVAS</h2>

    <canvas id="meucanvas" width="640" height="480">Opa! pode ser que seu navegador não suporte o canvas do HTML5.</canvas>

    <p></p>
    <hr>

    <h3>tchauzinho...</h3>
  </body>
</html>

4.2. Usando arquivos CSS para formatar elementos do HTML

Os arquivos de estilo no formato CSS (Cascading Stylesheets) facilitam a formatação de sua página.

Salve o trecho abaixo em um arquivo estilo.css (observe que o canvas.html carrega esse arquivo na seção <head> usando a tag <link>) na mesma pasta do seu arquivo canvas.html, e brinque um pouco, por exemplo, mudando algumas cores de black para red ou green. Para isso, abra o arquivo com um editor de texto de sua preferência.

A sintaxe do CSS é relativamente simples. Basta, para cada elemento, criar um dicionário de nomes e valores. Assim, para o elemento “canvas”, esse arquivo estilo.css define algumas de suas propriedades, nesse caso, “border” e “background-color”. Para o elemento “h3”, o arquivo define “color”.

Para saber mais, consulte materiais disponíveis na Internet, como esses tutoriais da Mozilla.

Para a maioria dos nossos exemplos e exercícios, o uso de CSS é facultativo e talvez seja suficiente definir as propriedades desejadas em tags de estilo (style).

4.2.1. Exemplo de um arquivo CSS

Listing 4.2 arquivo estilo.css
canvas {
    border: 2px solid black;
    background-color: yellow;
}
h3 {
    color: green;
}

4.3. Desenhando no canvas usando JavaScript

O JavaScript é uma linguagem de script (scripting language) criada para a Web. Toda vez que uma página mostra algo “dinâmico” (como updates de tempos em tempos) ou algo “interativo” (como um mapa), seu navegador deve estar executando um script (ou programa) em JavaScript.

O exemplo a seguir mostra o conteúdo do arquivo meu_script.js que foi usado no arquivo HTML fornecido anteriormente. Crie um arquivo com esse mesmo nome na mesma pasta dos arquivos anteriores e salve o código abaixo nesse arquivo.

4.3.1. Exemplo: arquivo meu_script.js

Listing 4.3 arquivo meu_script.js
/*
 * ==================================================================
 *
 *  Esqueleto de um script JavaScript
 *
 * ==================================================================
*/

// A `main()` só deve ser executada quando tudo estiver carregado
window.onload = main; // nome da função a ser chamada 'onload'

// Lista de variáveis globais
var ctx;  // contexto com a API para desenhar

/**
 * função principal
 */
function main() {
    // veja o canvas id definido no arquivo index.html
    const canvas = document.getElementById('meucanvas');
    // vamos definir um contexto para desenhar em 2D
    ctx = canvas.getContext('2d');
    if (!ctx) alert("Não consegui abrir o contexto 2d :-( ");

    let cor = 'blue';
    desenheRect( cor, 20, 40, 160, 80 );
    cor = 'red';
    desenheRect( cor, 100, 60, 340, 280 );
    cor = 'green';
    desenheRect( cor, 320, 240, 300, 220 );
};

// ==================================================================
//   outras funções

/**
 * recebe uma cor e os parâmetros de um retângulo e
 * desenha a região interna do retângulo com cor.
 * @param {string} cor 
 * @param {number} left - coluna esquerda
 * @param {number} top  - linha superior
 * @param {number} width - largura do retângulo
 * @param {number} height - altura
 */
function desenheRect( cor, left, top, width, height ) {
    console.log("Desenhando retângulo ", cor);
    ctx.fillStyle = cor;
    ctx.fillRect( left, top, width, height );
};

Nesse e em futuros exemplos de programas gráficos usando JavaScript vamos adotar um esqueleto e sintaxe que se assemelham a programas em C e Python. Além de facilitar o entendimento desses exemplos por programadores que dominam outras linguagens, esperamos com isso facilitar também sua transição para o JavaScript. Dessa forma, nossos exemplos vão conter uma função main() sempre definida no início do arquivo e que é chamada apenas quando o arquivo termina de ser carregado pelo navegador usando a chamada window.onload = main;.

Essa é a única função a ser chamada durante o carregamento do arquivo pelo navegador. Todas as demais funções são chamadas a partir da main().

Para definir uma função em JavaScript devemos usar o comando function. Suas demais funções podem ser definidas, em qualquer ordem, após a main().

Observe nesse exemplo que, ao programar em JavaScript, devemos também:

  • terminar cada comando com um ponto-e-vírgula (;);
  • indicar o início e fim de cada bloco usando chaves, a menos quando o bloco conter apenas um comando.

Em JavaScript, as variáveis precisam ser declaradas como var, let ou const. Use a declaração var define variáveis globais, use let para variáveis locais, ou seja, aquelas válidos apenas dentro da função ou bloco onde foram definidas. Use variáveis do tipo const para definir constantes, ou seja, valores que não devem ser alterados depois de criados.

Depois de criar os três arquivos (HTML, CSS e JavaScript) na mesma pasta, carregue o arquivo canvas.html em um navegador de sua preferência. A Figura Fig. 4.2 exibe o resultado desse nosso primeiro programa em JavaScript em uma janela do navegador Chrome.

Navegador Chrome exibindo o resultado do programa JavaScript

Fig. 4.2 Navegador Chrome exibindo o resultado do programa JavaScript. O canvas é exibido na metade esquerda e, à direita, está o console do JavaScript.

Aproveite também para abrir o console do JavaScript clicando, a partir da barra de menu do Chrome, em View -> Developer -> JavaScript Console. O console está também disponível em outros navegadores modernos, como o Firefox, Edge e Safari, e pode lhe ser muito útil para depurar os seus programas, por exemplo, exibindo mensagens usando o comando console.log() como mostrado na função desenheRect(). Antes porém de explicar o desenho, vamos entender melhor o comportamento da main().

4.3.2. O que faz a função main()?

As páginas da Web são estruturadas como objetos DOM (Document Object Model – modelo de objeto de documento), permitindo que os programas em JavaScript (ou em outra linguagem) possam ter acesso aos elementos da página. Esse documento é carregado automaticamente e o canvas pode então ser obtido pelo método document.getElementById(), usando o id definido no arquivo canvas.html (no caso, o valor de id definido no html foi meucanvas).

Associada ao canvas podemos fazer uso de APIs gráficas distintas para desenho, também chamadas de contextos. Nesse exemplo, a variável global ctx recebe um contexto para desenhos 2D. Em aulas futuras, vamos fazer uso do contexto webgl2, a API gráfica para desenhar cenas 3D usando o WebGL. Observe que, por ter sido declarada como global, ctx pode ser utilizada dentro da função desenheRect() diretamente, ao invés de passá-la como argumento da função. Como é possível que o contexto que você deseje utilizar não esteja disponível no navegador utilizado, é recomendado testá-lo antes de prosseguir. O programa envia um alerta caso o contexto não esteja disponível.

A função main() chama a função desenheRect() 3 vezes. Cada vez que a função é chamada, uma mensagem é impressa no console.log indicando a cor do retângulo a ser pintado.

Antes de desenhar algum elemento gráfico, como uma linha ou um retângulo, é necessário definir todas as propriedades desejadas do elemento. Nesse exemplo, apenas a cor usada para preenchimento foi definida no atributo fillStyle do contexto, antes de desenhar cada retângulo usando o método fillRect(). Métodos identificados como fill incluem o interior da região (no caso um retângulo), enquanto métodos identificados como stroke apenas desenham o contorno da região.

Modelo de plotter com caneta

Esse comportamento, de definir as condições (ou estado) das ferramentas de desenho, como cor do pincel ou lápis, grossura do pincel, tipo da linha (se contínua ou tracejada), etc. antes de desenhar é tipico de ferramentas de desenho 2D tradicionais conhecida como modelo de plotter com caneta (Pen Plotter). Os plotters são dispositivos ainda bastante utilizados para impressão de folhas grandes, usadas por exemplo para exibir a planta de uma casa ou edifício. Nesse modelo, devemos mover a caneta para uma determinada posição, baixar a caneta para que ela toque no papel e comece a desenhar, e arrastá-la (movê-la) para outras posições, desenhando as linhas que compõem o desenho. Note também a adequação desse modelo para desenhar imagens vetoriais.

Experimente desenhar apenas o contorno do retângulo, ao invés de preencher seu interior, usando o método strokeRect() (experimente, modificando o script.js e recarregando o canvas.html!).

O contexto 2D possui recursos simples para desenhar retângulos e outras figuras formadas por linhas. Permite também desenhar texto e imagens raster (carregadas de um arquivo, por exemplo).

4.4. Arrays no JavaScript

Arrays no JavaScript são estruturas sequenciais dinâmicas, muito semelhante ao tipo list do Python, ou seja, um mesmo array pode conter elementos de tipos distintos, que podem ser inseridos e removidos dinamicamente usando os métodos push() e pop(). O seguinte exemplo ilustra o uso de array para a definição de pontos que definem um polígono.

Listing 4.4 arquivo scrip2.js
/**
 *  Esse script mostra mais recursos do canvas e da JS
 */

// A `main()` só deve ser executada quando tudo estiver carregado
window.onload = main;

// variáveis globais
var ctx;  // contexto de desenho

//==================================================================
/**
 * função principal
 */
 function main() {
    // veja o canvas id definido no arquivo index.html
    const canvas = document.getElementById('meucanvas');
    // vamos definir um contexto para desenhar em 2D
    ctx = canvas.getContext('2d');
    if (!ctx) alert("Não consegui abrir o contexto 2d :-( ");

    let pontos = [ [20, 40], [180, 120], [180, 40], [20, 120] ];
    desenhePoligono( pontos );

    pontos = []  // array vazio
    pontos.push( [100, 60] ); // coloca um ponto 
    pontos.push( [440, 60] ); // experimente alterar 
    pontos.push( [440, 340] ); // a ordem para ver o que acontece!
    desenhePoligono( pontos, 'red' );

    pontos.push( [100, 340] ); // descomente essa linha
    desenhePoligono( pontos, 'black', 2 );

    cor = 'green';
    pontos = [ [320, 240], [620, 240] ] // array não começa vazio
    pontos.push( [320, 460] );
    pontos.push( [620, 460] );
    desenhePoligono( pontos, 'green', 0 );

   desenheTexto("Exemplo de stroke e fill", 120, 420, 36, 'magenta' );
};

//==================================================================
// outras funções
// ------------------------------------------------------------------
/**
 * desenha um poligono definido por pts e
 * preenchido com uma cor sólida caso wid = 0.
 * Caso contrário, desenha o contorno com lagura wid.
 * @param {array} pts - array de pontos
 * @param {string} cor - cor para pintar o poligono
 * @param {number} wid - largura da borda se wid>0.
 */
function desenhePoligono( pts, cor='blue', wid = 10 ) {
    let tam = pts.length;
    console.log("Desenhando poligono", cor, pts, tam);

    let poli = new Path2D();
    poli.moveTo( pts[0][0], pts[0][1] );
    for (let i = 1; i < pts.length; i++) {
        poli.lineTo( pts[i][0], pts[i][1] );
        console.log( pts[i][0], pts[i][1]  );
    }
    poli.closePath(); // cria um contorno fechado.

    if (wid > 0) { 
        ctx.strokeStyle = cor;
        ctx.lineWidth = wid;
        ctx.stroke( poli );
    }
    else { // wid <= 0 preenche o polígono
        ctx.fillStyle=cor;
        ctx.fill( poli );
    }
}

// ------------------------------------------------------------------
/**
 * recebe o texto msg e o desenha na posição (x,y) do canvas.
 * @param {string} msg 
 * @param {number} x 
 * @param {number} y 
 * @param {number} tam - tamanho da fonte
 * @param {string} cor - cor do texto
 */
function desenheTexto (msg, x, y, tam=24, cor = 'black') {
    ctx.fillStyle = cor;
    ctx.font = `${tam}px serif`;  
    ctx.fillText(msg, x, y);
}

4.4.1. Discussão

A função main() desse exemplo ilustra como criar e acessar os elementos de um array pontos. Você pode substituir o conteúdo do arquivo script.js com esse novo programa, e recarregar o canvas.html em seu navegador, para ver o resultado, que está ilustrado na Figura Fig. 4.3.

Um array pode ser criado com todos os valores entre colchetes ([ ]). O atributo length contém o tamanho do array. Um segundo polígono (triângulo em vermelho) é criado usando um array inicialmente vazio e cada elemento é inserido em seguida usando o método push(). Observe que o triângulo se torna um retângulo (em preto) ao inserir mais um canto. Observe também que a ordem dos pontos no array é importante na definição das linhas que formam o objeto.

Resultado desenhado no canvas usando stroke

Fig. 4.3 Resultado do novo scrip2.js usando arrays de pontos para desenhar polígonos.

Esse exemplo explora também o polimorfismo da função desenhePoligono(), chamando a função com e sem alguns dos parâmetros opcionais. No caso, os valores default de cor e wid são blue e 10, respectivamente.

A função desenhePoligono() recebe um array de pontos e define um path2D, um objeto no contexto 2d representado por uma sequência de linhas, também chamado de polilinhas (polylines). O método closePath() é usado para criar um contorno fechado, ou seja, o último ponto é conectado ao primeiro. Sem a chamada desse método, o contorno fica aberto. Experimente comentar a chamada desse método para ver o que acontece com o desenho.

Essas linhas (contorno do polígono) são desenhadas usando o método stroke() com a cor e a largura de linha passados como argumentos da função. No entanto, quando o parâmetro wid tem valor 0, a região interna é preenchida com a cor usando o método fill().

Além do uso de arrays, há vários detalhes também ilustrados nesse exemplo, como o uso dos comandos if-else e for, o uso de parâmetros opcionais na função desenhePoligono e o uso de stroke no canvas.

Outro recurso interessante oferecido pelo canvas é a possibilidade de desenhar textos. A função desenheTexto() mostra um exemplo desse recurso.

4.5. Mais sobre polilinhas

Uma polilinha (ou mais propriamente uma curva poligonal) é uma sequência finita de segmentos de linha unidos de ponta a ponta. Esses segmentos de linha são chamados de arestas e os pontos finais dos segmentos de linha são chamados de vértices. Um único segmento de linha é um caso especial.

  • Uma linha infinita, que se estende até o infinito em ambos os lados, geralmente não é considerada uma polilinha.
  • Uma polilinha é fechada se terminar onde começa.
  • É simples se não se auto-cruzar. Auto-cruzar inclui coisas como duas arestas se cruzando, um vértice se cruzando no interior de uma aresta ou mais de duas arestas compartilhando um vértice comum.
  • Uma polilinha fechada simples também é chamada de polígono simples. Se todos os seus ângulos internos forem no máximo \(180^o\), então é um polígono convexo.

Uma polilinha no plano pode ser representada simplesmente como uma sequência das coordenadas (x, y) de seus vértices. Isso é suficiente para codificar a geometria de uma polilinha. Em contraste, a maneira como a polilinha é renderizada é determinada por um conjunto de propriedades chamadas de atributos gráficos. Isso inclui elementos como cor, largura de linha e estilo de linha (sólida, pontilhada, tracejada) e como segmentos consecutivos são unidos.

A polilinha é uma forma “vetorizada” para representar formas (vector graphics). Além de permitir uma representação compacta e facilmente escalável, polilinhas são mais simples de serem desenhadas usando monitores baseados em tubos de raios catódicos.

4.5.1. Região interna e externa

Qualquer polilinha simples e fechada no plano define uma região que pode ser definida como interna e um externa. Este é um exemplo típico de um fato totalmente óbvio da topologia que é notoriamente difícil de provar. É chamado de teorema da curva de Jordan. Podemos preencher qualquer região com uma cor ou padrão repetitivo. Em alguns casos, a própria polilinha delimitadora também é desenhada (considerada interna).

Uma polilinha com “buracos” embutidos também define naturalmente uma região que pode ser preenchida. Na verdade, isso pode ser generalizado aninhando buracos dentro dos buracos (alternando a cor com a cor de fundo). Mesmo que uma polilinha não seja simples, é possível generalizar a noção de interior. Dado qualquer ponto, atire um raio ao infinito. Se ele cruzar a fronteira um número ímpar de vezes, será colorido. Se cruzar um número par de vezes, receberá a cor de fundo.

4.6. Onde estamos e para onde vamos

Começamos a introduzir as ferramentas de programação que vamos utilizar ao longo desse curso. Em particular, vamos adotar um esqueleto e sintaxe simples (ou semelhante a outras linguagens) para os programas em JavaScript para facilitar a leitura desses programas, e a escrita de futuros programas, por programadores e programadoras familiarizadas com outras linguagens.

Nessa aula vimos como criar desenhos usando o contexto 2d do canvas. Na próxima aula vamos apresentar recursos para criar desenhos interativos e introduzir o conceito de programação baseada em eventos.

4.7. Exercícios

  1. Escreva um programa em JavaScript (+ HTML) que desenha, em um canvas de tamanho \(300 \times 300\), a aproximação de um círculo de raio 100 usando 8 segmentos de reta. Depois disso, altere esse número para 16, 32, 64 segmentos para ver o que acontece com a qualidade visual desse círculo.

  2. Escreva um programa em JavaScript (+ HTML) que desenha, em um canvas de tamanho \(400 \times 400\), 50 elementos aleatórios de uma grade de dimensão \(10\times10\), com uma cor também aleatória.

    Para sortear um inteiro no intervalo [min, max], e uma cor RGB, você pode utilizar as seguintes funções em JS:

    function sorteie_inteiro (min, max) {
         return Math.floor(Math.random() * (max - min) ) + min;
     }
    
     function sorteie_corRGB () {
         let r = sorteie_inteiro(0, 255);
         let g = sorteie_inteiro(0, 255);
         let b = sorteie_inteiro(0, 255);
         return `rgb( ${r}, ${g}, ${b} )`;  // retorna uma string
     }
    

    A figura abaixo ilustra 3 exemplos de como deve aparecer seu canvas. Clique em reload várias vezes para gerar novos desenhos.

    Exemplos de uma grade pintada aleatoriamente

    Fig. 4.4 Exemplos de grades \(10\times10\) com aproximadamente 50% de seus elementos pintados de forma aleatória.

  3. Escreva um programa em JavaScript (+ HTML) que desenha, em um canvas de tamanho \(640 \times 480\), 10 quadrados de tamanhos e cores aleatórias, em posições também aleatórias.

4.8. Para saber mais

Para aprender mais detalhes dessas ferramentas recomendamos as seguintes leituras: