Este é o quinto artigo de uma série que irá apresentar os recursos disponibilizados pela nova tag <canvas> do HTML5. Nesse artigo, veremos
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 }
<canvas id="canvas" width="150" height="150"></canvas>
draw();
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, ey
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(); }
<canvas id="canvas" width="300" height="300"></canvas>
draw();
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(); } }
<canvas id="canvas" width="150" height="150"></canvas>
draw();
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(); }
<canvas id="canvas" width="300" height="300"></canvas>
draw();
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 x e y). 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:
-
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); }
<canvas id="canvas" width="200" height="250"></canvas>
draw();
Screenshot | Live sample |
---|---|
![]() |