Tutorial de Canvas – Parte 5 – Transformações

Este é o quinto artigo de uma série que irá apresentar os recursos disponibilizados pela nova tag <canvas> do HTML5. Nesse artigo, veremos como aplicar transformações aos objetos desenhados no Canvas. Você pode acessar uma lista de todos os artigos da série nesse link.

Índice do conteúdo

  1. Salvando e restaurando estado
    1. Um exemplo de salvamento e restauração do estado do canvas
  2. Translação
    1. Um exemplo com translate
  3. Rotação
    1. Um exemplo com rotate
  4. Escalonamento
    1. Um exemplo com scale
  5. Transformações
    1. Exemlos de transform / setTransform

Salvando e restaurando estado

Antes de partir para os métodos de transformações, vamos dar uma olhada em dois outros métodos que são indispensáveis quando se começa a gerar desenhos mais complexos.

save()
Salva todo o estado do canvas.
restore()
Restaura para o estado do canvas mais recente.

Os estados do canvas são armazenados em uma pilha. Cada vez que o método save() é chamado, o estado atual do desenho é colocado na pilha. Um estado de desenho consiste de

  • As transformações que foram aplicadas (isto é, translação, rotação e escala – veja a seguir).
  • Os valores das propriedades strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, shadowOffsetX, shadowOffsetY, shadowBlur, shadowColor, globalCompositeOperation.
  • O caminho de corte atual, que veremos nos próximos artigos desta série.

Podemos chamar o método save() quantas vezes quisermos. Cada vez que o método restore() é chamado, o último estado salvo é puxado da pilha e todas as configurações salvas são restauradas.

Um exemplo de salvamento e restauração do estado do canvas

Esse exemplo tenta ilustrar como a pilha de estados funciona através do desenho de um conjunto de retângulos consecutivos.

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

  ctx.fillRect(0,0,150,150);   // Draw a rectangle with default settings
  ctx.save();                  // Save the default state

  ctx.fillStyle = '#09F'       // Make changes to the settings
  ctx.fillRect(15,15,120,120); // Draw a rectangle with new settings

  ctx.save();                  // Save the current state
  ctx.fillStyle = '#FFF'       // Make changes to the settings
  ctx.globalAlpha = 0.5;    
  ctx.fillRect(30,30,90,90);   // Draw a rectangle with new settings

  ctx.restore();               // Restore previous state
  ctx.fillRect(45,45,60,60);   // Draw a rectangle with restored settings

  ctx.restore();               // Restore original state
  ctx.fillRect(60,60,30,30);   // Draw a rectangle with restored settings
}

O primeiro passo é desenhar um retângulo grande com as configurações padrões. Em seguida, salvamos esse estado e fazemos alterações na cor de preenchimento. Desenhamos então o segundo e menor retângulo e salvamos o estado. Novamente alteramos alguns ajustes de desenho e desenhamos o terceiro e semi-transparente retângulo branco.

Até agora, isto é bem parecido com o que fizemos nos artigos anteriores. Porém, quando você chama a primeira sentença restore() , o estado de desenho do topo da pilha é removido dela, e as configurações são restauradas. Se não tivéssemos salvo o estado com o método save(), precisaríamos alterar a cor e a transparência manualmente para poder retornar ao estado anterior. Isso pode ser fácil para duas propriedades, mas se tivermos mais do que isso, nosso código iria ficar muito grande rapidamente.

Quando a segunda sentença restore() é chamada, o estado original (aquele que configuramos antes da primeira chamada ao método save) é restaurado e o último retângulo é novamente desenhado na cor preta.

Screenshot Live sample

Translação

O primeiro dos métodos de transformação que veremos é o translate(). Esse método é usado para mover o canvas e sua origem para um ponto diferente do grid.

translate(x, y)
Move o canvas e sua origem no grid. x indica a distância horizontal a ser movida, e y indica quanto se deve mover no grid verticalmente.

É uma boa salvar o estado do canvas antes de executar qualquer transformação. Em muitos casos, é mais fácil apenas chamar o método restore  do que reverter uma translação para retorna ao estado original. Além disso, se estiver executando a translação dentro de um loop e não salvar e restaurar o estado do canvas, pode acabar perdendo parte de seu desenho, porque foi desenhado fora dos limites do canvas.

Um exemplo com translate

Esse exemplo demonstra alguns dos benefícios de transladar a origem do canvas. Criaremos uma função drawSpirograph() que desenha padrões em espiral. Esses padrões são desenhados em torno da origem. Sem a função translate() , veríamos apenas um quarto desses padrões no canvas. O método translate()  também nos dão liberdade para posiciona-los em qualquer local do canvas sem ter que ajustar manualmente as coordenadas na função de espiral. Isso torna o entendimento e o uso dela um pouco mais fácil.

Na função draw(), chamamos  drawSpirograph() nove vezes usando dois loops for . Em cada loop, o canvas é transladado, uma espiral é desenhada, e o canvas retorna para o estado original.

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.fillRect(0,0,300,300);
  for (var i=0;i<3;i++) {
    for (var j=0;j<3;j++) {
      ctx.save();
      ctx.strokeStyle = "#9CFF00";
      ctx.translate(50+j*100,50+i*100);
      drawSpirograph(ctx,20*(j+2)/(j+1),-8*(i+3)/(i+1),10);
      ctx.restore();
    }
  }
}

function drawSpirograph(ctx,R,r,O){
  var x1 = R-O;
  var y1 = 0;
  var i  = 1;
  ctx.beginPath();
  ctx.moveTo(x1,y1);
  do {
    if (i>20000) break;
    var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
    var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
    ctx.lineTo(x2,y2);
    x1 = x2;
    y1 = y2;
    i++;
  } while (x2 != R-O && y2 != 0 );
  ctx.stroke();
}
Screenshot Live sample

Rotação

O segundo método de transformação é o rotate(). Usamos ele para rotacionar o canvas em torno da origem atual.

rotate(angle)
Rotaciona o canvas no sentido horário em torno da origem atual com o ângulo angle em radianos.

O ponto central de rotação sempre é o ponto de origem. Para alterar o ponto central, precisamos mover o canvas usando o método translate().

Um exemplo com rotate

Nesse exemplo, usaremos o método rotate() para desenhar formas em um padrão circular. Você poderia também calcular coordenadas para x e y individualmente (x = r*Math.cos(a); y = r*Math.sin(a)). Nesse caso, não importa qual método você escolher, estaria desenhando círculos.  Calcular as coordenadas resulta apenas em rotacionar as posições centrais e não os círculos, enquanto ao usar o método rotate() o resultado são as duas coisas, mas naturalmente círculos parecem os mesmo não importando o quanto eles são rotacionados em relação ao seu centro.

Novamente, temos dois loops. O primeiro determina o número de anéis, e o segundo determina o número de pontos desenhados em cada anel. Antes de desenhar cada anel, salvamos o estado do canvas, de forma que possamos recuperar esse estado facilmente. Para cada ponto desenhado, rotacionamos o espaço de coordenadas do canvas por um ângulo que é determinado pelo número de pontos do anel. O círculo mais interno possui seis pontos, então a cada passo rotacionamos com um ângulo de 360/6 ou 60 graus. A cada anel adicional, o número de pontos dobra, e ângulo cai pela metade.

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

  for (var i=1;i<6;i++){ // Loop through rings (from inside to out)
    ctx.save();
    ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';

    for (var j=0;j<i*6;j++){ // draw individual dots
      ctx.rotate(Math.PI*2/(i*6));
      ctx.beginPath();
      ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
      ctx.fill();
    }

    ctx.restore();
  }
}
Screenshot Live sample

Escalonamento

O próximo método de transformação é o escalonamento. Usamos ele para aumentar ou diminuir as unidades de nosso grid. Isso pode ser usado para aumentar ou diminuir formas e bitmaps.

scale(x, y)
Escala as unidades do canvas por x horizontalmente e por y verticalmente. Os dois parâmetros são números reais. Números negativos reduzem o tamanho da unidade e valores positivos aumentam o tamanho. O valor 1.0 deixa as unidades com o mesmo tamanho.

Ao usar número negativos, você pode fazer espelhamento do eixo (por exemplo, ao usar translate(0,canvas.height); scale(1,-1); você terá o bem conhecido sistema de coordenadas cartesianas, com a origem no canto inferior esquerdo).

Por padrão, uma unidade do canvas é exatamente um pixel. Se aplicarmos, por exemplo, um fator de escalonamento de 0.5, a unidade resultante seria 0.5 pixels e assim as formas seriam desenhadas com metade do tamanho. De forma similar, configurando um fator de escalonamento de 2.0 aumentaria o tamanho da unidade e uma unidade seria dois pixels. Isso resultaria no desenho das formas duas vezes maiores.

Um exemplo com scale

Nesse último exemplo, usaremos a função de espirais do exemplo anterior para desenhar nove formas com diferentes fatores de escalonamento.

function draw() {
  var ctx = document.getElementById('canvas').getContext('2d');
  ctx.strokeStyle = "#fc0";
  ctx.lineWidth = 1.5;
  ctx.fillRect(0,0,300,300);

  // Uniform scaling
  ctx.save()
  ctx.translate(50,50);
  drawSpirograph(ctx,22,6,5);

  ctx.translate(100,0);
  ctx.scale(0.75,0.75);
  drawSpirograph(ctx,22,6,5);

  ctx.translate(133.333,0);
  ctx.scale(0.75,0.75);
  drawSpirograph(ctx,22,6,5);
  ctx.restore();

  // Non uniform scaling (y direction)
  ctx.strokeStyle = "#0cf";
  ctx.save()
  ctx.translate(50,150);
  ctx.scale(1,0.75);
  drawSpirograph(ctx,22,6,5);

  ctx.translate(100,0);
  ctx.scale(1,0.75);
  drawSpirograph(ctx,22,6,5);

  ctx.translate(100,0);
  ctx.scale(1,0.75);
  drawSpirograph(ctx,22,6,5);
  ctx.restore();

  // Non uniform scaling (x direction)
  ctx.strokeStyle = "#cf0";
  ctx.save()
  ctx.translate(50,250);
  ctx.scale(0.75,1);
  drawSpirograph(ctx,22,6,5);

  ctx.translate(133.333,0);
  ctx.scale(0.75,1);
  drawSpirograph(ctx,22,6,5);

  ctx.translate(177.777,0);
  ctx.scale(0.75,1);
  drawSpirograph(ctx,22,6,5);
  ctx.restore();

}
function drawSpirograph(ctx,R,r,O){
  var x1 = R-O;
  var y1 = 0;
  var i  = 1;
  ctx.beginPath();
  ctx.moveTo(x1,y1);
  do {
    if (i>20000) break;
    var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
    var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
    ctx.lineTo(x2,y2);
    x1 = x2;
    y1 = y2;
    i++;
  } while (x2 != R-O && y2 != 0 );
  ctx.stroke();
}

A forma da pare superior esquerda é desenhada sem nenhuma escala aplicada. As formas em amarelo a direita possuem um fator de escalonamento uniforme (o mesmo valor para os parâmetros xy). Se você analisar o código acima verá que usamos o método  scale()  duas vezes com valores iguais seus parâmetros para as segunda e terceira espirais. Por não termos restaurado o estado do canvas, a terceira forma é desenhada com um fator de escalonamento de 0.75 × 0.75 = 0.5625.

A segunda linha de formas azuis possui um escalonamento não uniforme aplicado na direção vertical. Cada uma das formas possui o fator de escalonamento para x configurado para 1.0, o que significa nenhum escalonamento. O fator de escalonamento para y é 0.75. Isso resulta em três formas sendo achatadas. A forma que era circular torna-se uma elipse. Se você observar atentamente verá que a espessura da linha também foi reduzida na direção vertical.

A terceira linha de formas verdes são parecidas com as de cima, mas agora aplicamos o escalonamento na direção horizontal.

Screenshot Live sample

Transformações

Os métodos de transformações finais permitem modificação diretas na matriz de transformações.

transform(m11, m12, m21, m22, dx, dy)
Esse método multiplica a matriz de transformação atual pela matriz descrita por:
equation
m11 	m21 	dx
m12 	m22 	dy
0 	0 	1
Se qualquer um dos argumento for Infinity a matriz de transformação precisa ser marcada como tal, senão o método irá dispará uma exceção.
setTransform(m11, m12, m21, m22, dx, dy)
Reseta a matriz de transformação atual para a matriz identidade, e então chama o método transform() com os mesmo argumento. Isso basicamente desfaz a transformação atual, e configura a transformação especificada, tudo em um único passo.

Exemplos de transform / setTransform

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

  var sin = Math.sin(Math.PI/6);
  var cos = Math.cos(Math.PI/6);
  ctx.translate(100, 100);
  var c = 0;
  for (var i=0; i <= 12; i++) {
    c = Math.floor(255 / 12 * i);
    ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
    ctx.fillRect(0, 0, 100, 10);
    ctx.transform(cos, sin, -sin, cos, 0, 0);
  }

  ctx.setTransform(-1, 0, 0, 1, 100, 100);
  ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
  ctx.fillRect(0, 50, 100, 100);
}
Screenshot Live sample