Tutorial de Canvas – Parte 2 – Desenhando formas

Este é o segundo artigo de uma série que irá apresentar os recursos disponibilizados pela nova tag <canvas> do HTML5. Nesse artigo, veremos como desenhar formas simples com o Canvas (como triângulos, linhas e arcos). Você pode acessar uma lista de todos os artigos da série nesse link.

Índice do conteúdo

  1. O grid
  2. Desenhando retângulos
    1. Exemplo de forma retângular
  3. Desenhando caminhos
    1. Desenhando um triângulo
    2. Movendo o pincel
    3. Linhas
    4. Arcos
    5. Curvas de Bezier e quadráticas
      1. Curvas de Bezier Quadráticas
      2. Curvas de Bezier Cúbicas
    6. Retângulos
    7. Criando combinações

O grid

Antes de podermos começar a fazer desenhos, precisamos falar um pouco sobre o grid do canvas ou espaço de coordenadas. O modelo do HTML do artigo anterior possuía um elemento canvas de 150 pixels de largura por 150 pixels de altura. À direita, você pode visualizar esse canvas com o grid padrão destacado. Normalmente 1 unidade no grid corresponde a 1 pixel no canvas. A origem desse grid é posicionada no canto superior esquerdo (coordenada (0,0)). Todos os elementos são posicionados relativamente a essa origem. Assim, a posição do canto superior esquerdo do quadrado azul torna-se x pixels da esquerda e y pixels do topo (coordenada (x,y)). Mais pra frente veremos como transladar a origem para posições diferentes, rotacionar o grid ou mesmo escalona-lo. Nesse momento, vamos nos ater ao padrão.

Desenahdo retângulos

Ao contrário do SVG, o <canvas> suporta apenas uma forma primitiva: retângulos. Todas as outras formas precisam ser criadas pela combinação de um mais caminhos. Por sorte, temos um conjunto de funções para desenho de caminhos que torna possível compor formas bastante complexas.

Em primeiro lugar, vamos dar um olhada no retângulo. Existem três funções que permitem desenhar retângulos nos canvas:

fillRect(x, y, width, height)
Desenha um retângulo com preenchimento.
strokeRect(x, y, width, height)
Desenha o contorno de um retângulo.
clearRect(x, y, width, height)
Limpa a área retangular especificada, tornando-a totalmente transparente.

Cada uma dessas três funções pede os mesmos parâmetros.. x e y especificam a posição no canvas (relativa a origem) do canto superior esquerda do retângulo. width e height fornecem o tamanho do retângulo.

A seguir temos a função draw() do artigo anterior, mas agora fazendo uso dessas três funções.

Exemplo de forma retangular

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    ctx.fillRect(25,25,100,100);
    ctx.clearRect(45,45,60,60);
    ctx.strokeRect(50,50,50,50);
  }
}

A saída desse exemplo é mostrado a seguir.

Screenshot Live sample

A função fillRect() desenha um quadrado preto de 100 pixels em cada lado. A função clearRect() então apaga uma região de 60×60 pixels no centro dele, e por fim strokeRect() é chamada para criar um contorne retangular de 50×50 pixels dentro da área que foi apagada.

Nos próximos artigos veremos dois métodos alternativos para clearRect(), e também veremos como alterar a cor e o estilo da linha dessas formas.

Ao contrário das funções de caminho que veremos na próxima seção, todas as três funções são desenhadas imediatamente no canvas.

Desenhando caminhos

Para criar formas usando caminhos são precisos alguns passos extras. Em primeiro lugar, você cria o caminho. Em seguida, usa comandos de desenho para desenhar o caminho. Depois, fecha o caminho. Uma vez que o caminho tenha sido criado, você pode preencher o caminho para renderiza-lo. A seguir as funções para fazer isso:

beginPath()
Cria um novo caminho. Uma vez criado, comandos de desenho futuros são direcionados ao caminho e usados para construí-lo.
closePath()
Fecha o caminho de forma que futuros comandos de desenho sejam direcionados novamente ao contexto.
stroke()
Desenha apenas o contorno da forma.
fill()
Desenha uma forma sólida pelo preenchimento da área contida dentro do caminho.

O primeiro passo para criar um caminho é chamar beginPath(). Internamente, caminhos são armazenados como uma lista de sub-caminhos (linhas, arcos, etc) que juntas forma uma forma. A cada vez que esses método é chamado, a lista é resetada e podemos desenhar novas formas.

Nota: Quando o caminho atual estiver vazio, como imediatamente após a chamada à beginPath(), ou em um canvas criado recentemente, o primeiro comando de construção de caminho é sempre tratado como um moveTo(), não importando o que ele seja de fato. Por essa razão, você quase sempre irá querer especificar seu ponto inicial após resetar um caminho.

O segundo passo é a chamada a métodos que irá de fato especificar os caminhos a serem desenhados. Veremos esses métodos a seguir.

O terceiro passo (opcional) é chamar closePath(). Esse método tenta fechar a forma pelo desenho de uma linha reta do ponto atual ao ponto inicial. Se a forma já estiver fechada ou houver apena um ponto na lista, essa função não faz nada.

Nota: Quando você chama fill(), qualquer forma em aberto é fechada automaticamente, assim você precisa chamar closePath(). Esse não é o caso quando se usa stroke().

Desenhando um triângulo

Por exemplo, o código para desenhar um triângulo deve parecer com o seguinte:

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    ctx.beginPath();
    ctx.moveTo(75,50);
    ctx.lineTo(100,75);
    ctx.lineTo(100,25);
    ctx.fill();
  }
}

O resultado deve ser esse:

Movendo o pincel

Uma função muito útil, que não desenhada nada de fato mas torna-se parte da lista de caminhos descrita acima, é a função moveTo(). Você pode pensar nela como o ato de levantar o pincel ou caneta de um ponto do papel e posiciona-lo em outro.

moveTo(x, y)
Move o pincel para a coordenada especificada por x e y.

Quando o canvas é inicializado ou beginPath() é chamada, normalmente você irá querer usar a função moveTo() para posicionar o ponto inicial em algum lugar. Poderíamos usar moveTo() para desenhar caminhos desconexos. Dê uma olhada no smiley abaixo.As linhas vermelhas marcam os ponto onde o método moveTo() foi usado.

Para testar isso você mesmo, pode usar o trecho de código a seguir. Apenas cole ele sobre a função draw() que vimos anteriormente.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    ctx.beginPath();
    ctx.arc(75,75,50,0,Math.PI*2,true); // Outer circle
    ctx.moveTo(110,75);
    ctx.arc(75,75,35,0,Math.PI,false);   // Mouth (clockwise)
    ctx.moveTo(65,65);
    ctx.arc(60,65,5,0,Math.PI*2,true);  // Left eye
    ctx.moveTo(95,65);
    ctx.arc(90,65,5,0,Math.PI*2,true);  // Right eye
    ctx.stroke();
  }
}

O resultado deve ser o seguinte:

Live sample

Se você quiser visualizar as linhas de conexão, pode remover do código as linhas com as chamadas à moveTo().

Linhas

Para desenhar linhas retas, use o método lineTo().

lineTo(x, y)
Desenha uma linha a partir da posição atual até a posição especifica por x e y.

Esse método pede dois argumento, x e y, que são as coordenadas do ponto final da linha. O ponto inicial depende do caminho previamente desenhado, onde o ponto final do caminho anterior é o ponto inicial do seguinte, e assim em diante. O ponto inicial também pode ser definido pelo método moveTo().

O exemplo abaixo desenha dois triângulos, um preenchido e apenas o contorno do outro.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    // Filled triangle
    ctx.beginPath();
    ctx.moveTo(25,25);
    ctx.lineTo(105,25);
    ctx.lineTo(25,105);
    ctx.fill();

    // Stroked triangle
    ctx.beginPath();
    ctx.moveTo(125,125);
    ctx.lineTo(125,45);
    ctx.lineTo(45,125);
    ctx.closePath();
    ctx.stroke();
  }
}

Tudo isso começa pela chamada a beginPath() para iniciar uma nova forma. Usamos então o método moveTo() para mover o ponto inicial para a posição desejada. Após isso, duas linhas são desenhadas, que formarão os dois lados do triângulo.

Screenshot Live sample

Você pode observar a diferença entre o triângulo preenchido e o vazio. Isso é, como mencionado anteriormente, as formas são automaticamente fechada quando um caminho é preenchido, mas não são quando ele é um contorno. Se não colocarmos o closePath() para o contorno do triângulo, apenas duas linhas serão desenhadas, não um triângulo completo.

Arcos

Para desenhar arcos ou cícrculos, usamos o método arc() . Você pode usar também  arcTo(), mas a sua implementação é de alguma forma menos confiável, assim esta última função não será vista aqui.

arc(x, y, radius, startAngle, endAngle, anticlockwise)
Desenha um arco.

Esse método precisa de cinco parâmetros: x e y são as coordenadas do centro do circulo sobre o qual o arco deve ser desenhado, radius é auto-explicativo. Os parâmetros startAngle e endAngle definem os pontos de inicio e fim do arco em radianos, juntamente com a curva do circulo. Esses parâmetros são medidos a partir do eixo x. O parâmetro anticlockwise é um valor Booleano que, quando for true, desenha o arco no sentido anti-horário; caso contrário, o arco é desenhado no sentido horário.

Nota: Os ângulos na função arc  são medidos em radianos, e não graus. Para converter graus em radianos, você pode usar a seguinte expressão JavaScript radians = (Math.PI/180)*degrees.

O exemplo a seguir é um pouco mais complexo do que os visto anteriormente. Ele desenha 12 arcos diferentes todos com ângulos e preenchimentos diferentes.

Os dois loops for tem por objetivo percorrer as linhas e colunas de arcos. Para cada arco, iniciamos um novo caminho pela chamada à beginPath(). No código, cada parâmetro do arco está em um variável para um melhor entendimento, mas isso não é necessariamente feito dessa forma na vida real.

As coordenadas x e y devem ser claras o bastante. radius e startAngle são fixos. endAngle começa em 180 graus (metade de um círculo) na primeira coluna e é incrementado em passos de 90 graus, culminando em um circulo completo na última coluna.

A sentença para o parâmetro clockwise resulta no desenho da primeira e terceira linha serem desenhadas no sentido horário e a segunda e quarta no sentido anti-horário. Finalmente, a sentença  if faz com que os arcos da metade superior sejam contornos e os arcos da metade inferior sejam preenchidos.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    for(var i=0;i<4;i++){
      for(var j=0;j<3;j++){
        ctx.beginPath();
        var x              = 25+j*50;               // x coordinate
        var y              = 25+i*50;               // y coordinate
        var radius         = 20;                    // Arc radius
        var startAngle     = 0;                     // Starting point on circle
        var endAngle       = Math.PI+(Math.PI*j)/2; // End point on circle
        var anticlockwise  = i%2==0 ? false : true; // clockwise or anticlockwise

        ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise);

        if (i>1){
          ctx.fill();
        } else {
          ctx.stroke();
        }
      }
    }
  }
}
Screenshot Live sample

 

Curvas de Bezier e quadráticas

Os próximos tipos de caminhos disponíveis são as Curvas de Bézier, disponíveis tanto na variação quadrática quanto na cúbica. Essas formas são geralmente usadas para desenhar formas orgânicas mais complexas.

quadraticCurveTo(cp1x, cp1y, x, y)
Desenha uma curva de Bézier quadrática da posição atual do pincel até a posição especificada por x e y, usando o ponto de controle especificado por cp1x e cp1y.
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
Desenha uma curva de Bézier cúbica da posição atual do pincel até a posição especificada por x e y, usando o ponto de controle especificado por (cp1x, cp1y) e (cp2x, cp2y).

A diferença entre entre essas duas funções pode ser melhor demonstrada pela imagem à direita. Uma curva de Bézier quadrática possui um ponto inicial e um final (pontos azuis) e apenas um ponto de controle (indicado pelo ponto vermelho) enquanto a curva de Bézier cúbica possui dois pontos de controle.

Os parâmetros x e y dos dois métodos são coordenadas do ponto final. cp1x e cp1y são coordenadas do primeiro ponto de controle, e cp2x e cp2y são coordenadas no segundo ponto de controle.

O uso do curvas de Bézier quadráticas e cúbicas pode ser bastante desafiador, porque ao contrário de softwares de desenho vetorial como o Adobe Illustrator, não temos um feedback visual direto do que estamos fazendo. Isso torna bem difícil desenhar formas complexas. No exemplo a seguir, desenharemos algumas formas orgânicas simples, mas se você tiver tempo e acima de tudo, paciência, formas muito mais complexas podem ser criadas.

Não há nada muito difícil nesses exemplos. Em ambos os casos veremos uma sucessão de curvas sendo desenhadas que resultará em uma forma completa.

Curvas de Bezier Quadráticas

Esse exemplo usa múltiplas curvas de Bézier quadráticas para renderizar um balão de diálogo.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');

    // Quadratric curves example
    ctx.beginPath();
    ctx.moveTo(75,25);
    ctx.quadraticCurveTo(25,25,25,62.5);
    ctx.quadraticCurveTo(25,100,50,100);
    ctx.quadraticCurveTo(50,120,30,125);
    ctx.quadraticCurveTo(60,120,65,100);
    ctx.quadraticCurveTo(125,100,125,62.5);
    ctx.quadraticCurveTo(125,25,75,25);
    ctx.stroke();
  }
}
Screenshot Live sample

Curvas de Bezier Cúbicas

Esse exemplo desenha um coração usando curvas de Bézier cúbicas.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    // Quadratric curves example
    ctx.beginPath();
    ctx.moveTo(75,40);
    ctx.bezierCurveTo(75,37,70,25,50,25);
    ctx.bezierCurveTo(20,25,20,62.5,20,62.5);
    ctx.bezierCurveTo(20,80,40,102,75,120);
    ctx.bezierCurveTo(110,102,130,80,130,62.5);
    ctx.bezierCurveTo(130,62.5,130,25,100,25);
    ctx.bezierCurveTo(85,25,75,37,75,40);
    ctx.fill();
  }
}
Screenshot Live sample

Retângulos

Além dos três métodos que vimos na seção Desenhando retângulos, que desenhavam formas retangulares diretamente no canvas, existe um métodorect(), que adiciona um caminho retangular em um caminho aberto.

rect(x, y, width, height)
Desenha um retângulo cujo canto superior esquerdo é especificado por (x, y) com largura width e altura height.

Quando esse método é executado, o método  moveTo() é chamado automaticamente com os parâmetros (0,0). Em outras palavras, a posição atual do pincel é resetada automaticamente para as coordenadas padrão.

Criando combinações

Até agora, cada exemplo dessa página tem usado apenas um tipo de função de caminho por forma. Porém, não há limitação quanto ao número de tipo de caminhos que podem ser usados para criar uma forma. Nesse exemplo final, vamos combinar todas as funções de caminho para criar um conjunto muito famoso de caracteres de um jogo.

function draw() {
  var canvas = document.getElementById('canvas');
  if (canvas.getContext){
    var ctx = canvas.getContext('2d');

    roundedRect(ctx,12,12,150,150,15);
    roundedRect(ctx,19,19,150,150,9);
    roundedRect(ctx,53,53,49,33,10);
    roundedRect(ctx,53,119,49,16,6);
    roundedRect(ctx,135,53,49,33,10);
    roundedRect(ctx,135,119,25,49,10);

    ctx.beginPath();
    ctx.arc(37,37,13,Math.PI/7,-Math.PI/7,false);
    ctx.lineTo(31,37);
    ctx.fill();

    for(var i=0;i<8;i++){
      ctx.fillRect(51+i*16,35,4,4);
    }

    for(i=0;i<6;i++){
      ctx.fillRect(115,51+i*16,4,4);
    }

    for(i=0;i<8;i++){
      ctx.fillRect(51+i*16,99,4,4);
    }

    ctx.beginPath();
    ctx.moveTo(83,116);
    ctx.lineTo(83,102);
    ctx.bezierCurveTo(83,94,89,88,97,88);
    ctx.bezierCurveTo(105,88,111,94,111,102);
    ctx.lineTo(111,116);
    ctx.lineTo(106.333,111.333);
    ctx.lineTo(101.666,116);
    ctx.lineTo(97,111.333);
    ctx.lineTo(92.333,116);
    ctx.lineTo(87.666,111.333);
    ctx.lineTo(83,116);
    ctx.fill();

    ctx.fillStyle = "white";
    ctx.beginPath();
    ctx.moveTo(91,96);
    ctx.bezierCurveTo(88,96,87,99,87,101);
    ctx.bezierCurveTo(87,103,88,106,91,106);
    ctx.bezierCurveTo(94,106,95,103,95,101);
    ctx.bezierCurveTo(95,99,94,96,91,96);
    ctx.moveTo(103,96);
    ctx.bezierCurveTo(100,96,99,99,99,101);
    ctx.bezierCurveTo(99,103,100,106,103,106);
    ctx.bezierCurveTo(106,106,107,103,107,101);
    ctx.bezierCurveTo(107,99,106,96,103,96);
    ctx.fill();

    ctx.fillStyle = "black";
    ctx.beginPath();
    ctx.arc(101,102,2,0,Math.PI*2,true);
    ctx.fill();

    ctx.beginPath();
    ctx.arc(89,102,2,0,Math.PI*2,true);
    ctx.fill();
  }
}

// A utility function to draw a rectangle with rounded corners.

function roundedRect(ctx,x,y,width,height,radius){
  ctx.beginPath();
  ctx.moveTo(x,y+radius);
  ctx.lineTo(x,y+height-radius);
  ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
  ctx.lineTo(x+width-radius,y+height);
  ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
  ctx.lineTo(x+width,y+radius);
  ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
  ctx.lineTo(x+radius,y);
  ctx.quadraticCurveTo(x,y,x,y+radius);
  ctx.stroke();
}

A imagem resultante deve ser a seguinte:

Não entraremos em detalhe, já que é surpreendentemente simples. Os aspectos importantes a serem observados são o uso da propriedade fillStyle no contexto de desenho, e o uso de uma função utilitária (nesse caso, roundedRect()). O uso de funções utilitárias para pedaços do desenho que você está fazendo frenquentemente podem ser bastante úteis e reduzem a quantidade de código que você precisa escrever, assim a complexidade dele.

Iremos dar um olhada em fillStyle, com mais detalhes, mais à frente nessa série de artigos. Por agora, tudo que estamos fazendo é alterando a cor de preenchimento dos caminhos do padrão preto e brando e então voltando novamente para ele.