Máscara de imagens com Canvas do HTML5

{lang: 'pt-BR'}

Antes do HTML5, criar uma máscara para uma imagem era algo que exigia, necessariamente, a manipulação do arquivo em uma ferramenta gráfica externa, usando a  transparência do formato PNG. Isso atende muitos casos, mas não permite criar máscaras dinâmicas, ou seja, máscaras criadas via Javascript ou que mudem ao longo do tempo, como em uma animação por exemplo. É possível criar uma máscara usando uma DIV, mas isso só atende a problemas onde a máscara é regular (retangular). Se você quiser fazer uma animação com uma máscara no formato de um círculo ou de um polígono irregular, precisará lançar mão do Canvas do HTML5.

A primeira técnica para criar uma máscara usando canvas é a mais simples e imediata, mas tem alguns problemas de performance. Para fazer animações mais sofisticadas é preciso usar uma outra técnica, a mesma usada em alguns jogos (Bit Blit). Vou apresentar as duas técnicas, no intuito de identificar as diferenças entre elas.

Primeira técnica: clip()

Esta técnica compreende o desenho de um polígono qualquer, sucedido pela chamada do método context.clip(). Qualquer elemento desenhado em seguida, será recortado pelo polígono desenhado anteriormente. É importante que o polígono seja fechado e que seja preenchido com alguma cor.

<canvas class="image" id="canvas1" width="640" height="300"></canvas>

<script type="text/javascript">
    var imageTop = new Image(); 

    // Obter o objeto de canvas e seu contexto de desenho 2D
    var canvas1 = document.getElementById("canvas1");
    var ctx = canvas1.getContext("2d"); 

    /* Circulo */
    ctx.arc(300, 150, 150, 0, Math.PI*2, false);
    ctx.fillStyle = "#FFFFFF";
    ctx.fill();
    //  Criar uma máscara
    ctx.clip(); 

    imageTop.src = "foto1.jpg";
    // Aguarde a imagem carregar
    imageTop.onload = function(){
        // Desenhar a imagem no canvas
        ctx.drawImage(imageTop, 0, 0);
    }
</script>

O exemplo acima começa instanciando um objeto de imagem, que irá guardar o conteúdo da imagem que receberá a máscara. Em seguida acessamos o contexto 2D do canvas criado. Com a API de Canvas podemos fazer inúmeros desenhos. Em nosso exemplo fizemos um círculo completo com um preenchimento em branco. Apesar da cor branca não aparecer, isso influencia, em parte, as bordas da máscara. O método clip() define que qualquer coisa que for desenhada no canvas a partir de então, será ‘recortado’ pela máscara. Em seguida definimos a URL da imagem e, ao ser carregada, desenhamos a imagem no canvas.

Esta me parece a técnica mais simples de se entender e de implementar, mas sofre com performance quando resolvemos animar a máscara, em especial círculos. Além disso algumas pessoas podem reclamar da falta de resolução no contorno.

Segunda técnica: Bit Blit

A técnica de Bit Blit é bem consagrada em jogos, para aumentar a performance de renderização dos componentes gráficos. Eu mesmo já escrevi um artigo de como explorar a técnica usando Flash (Aumento da Performance de Jogos em Flash com Bit blitting). A idéia básica é usar operações de bit com os pixels dos elementos gráficos, de tal forma que possamos gerar a imagem final com operações mais leves.

Como estamos interessados em criar máscaras, vamos fazer desenhos com uma cor sólida (preto). Usando a operação de ‘source-atop’ vamos renderizar nossa imagem por cima do nosso desenho. O canvas entenderá que o elemento gráfico que sucede a operação ‘source-atop’ deverá ter uma interseção com o que foi desenhado previamente, ou seja, somente a área da imagem que estiver por cima do elemento gráfico em preto que desenhamos aparecerá no canvas.

<canvas class="image" id="canvas1" width="640" height="300"></canvas>

<script type="text/javascript">
    var image2 = new Image(); 
    var canvas2 = document.getElementById("canvasImage2"); 
    var ctx2 = canvas2.getContext("2d"); 
    image2.src = "foto2.jpg"; 
    image2.onload = function(){ 
	 ctx2.fillStyle = "#000000"; 
	 ctx2.fillRect(100, 100, 400, 200); 
	 ctx2.globalCompositeOperation = 'source-atop'; 
 	 ctx2.drawImage(image2, 0, 00); 
    } 
</script>

Assim como o exemplo anterior, neste esperamos carregar a imagem e em seguida fazemos o desenho de uma figura geométrica. Usamos o método context.fillRect(x, y, larg, alt) para desenhar um retângulo. O atributo context.globalCompositeOperation = 'source-atop' indica que qualquer elemento gráfico renderizado posteriormente deverá ter uma interseção com o que já foi desenhado.

Do ponto de vista de performance, as duas técnicas são similares quando trabalhamos com polígonos simples, como retângulos. No entando, ao desenhar círculos com o método arc() temos uma queda significativa de performance. É aí que a técnica de Bit blit entra e nos ajuda bastante. Por incrível que pareça, desenhar um quadrado com um degradê circular (radial) é mais rápido do que desenhar um arco. Sabendo disso, podemos desenhar um degradê radial com o mesmo diâmetro do círculo, dentro de um quadrado, usando o método context.createRadialGradient(startX, startY, startR, endX, endY, endR). Como estamos usando um degradê, temos a vantagem de podermos suavizar as bordas, tornando os contornos mais suaves.

<canvas class="image" id="canvas1" width="640" height="300"></canvas>

<script type="text/javascript">
var image3 = new Image(); 
var canvas3 = document.getElementById("canvasImage3"); 
var ctx3 = canvas3.getContext("2d"); 
image3.src = "http://blog.werneckpaiva.com.br/exemplos/mask/foto2.jpg"; 
image3.onload = function(){
        var radius = 100;
        var diameter = radius * 2;
	var x = 110;
	var y = 80;
	var r = ctx3.createRadialGradient(x + radius, y + radius, radius-5, x + radius, y + radius, radius);
	r.addColorStop(0.5, '#000');	
	r.addColorStop(0.95, 'rgba(0,0,0,0)');
	ctx3.fillStyle = r;
	ctx3.fillRect(x, y, diameter, diameter);
	ctx3.globalCompositeOperation = 'source-atop'; 
	ctx3.drawImage(image3, 0, -100); 
} 
</script>

Apos carregar a imagem, definimos um raio e um ponto x e y, onde o quadrado será posicionado. Em seguida criamos um preenchimento do tipo RadialGradient que é composto por dois círculos (var r = ctx3.createRadialGradient(x + radius, y + radius, radius-5, x + radius, y + radius, radius)). Quanto menor o círculo mais interno, mais ‘esfumaçado’ será a borda do círculo e, no exemplo, a diferença entre eles é de 5px. Como o gradiente pode ter várias cores, definimos uma cor sólida até 95%. A partir deste ponto, o degradê passa a ser transparente. Aplicamos o objeto de degradê criado ao estilo de preenchimento (ctx3.fillStyle = r). Desenhamos, então, o quadrado no ponto x, y e, por fim, usamos o mesmo recurso de operação bitmap usado no exemplo anterior (ctx3.globalCompositeOperation = 'source-atop') para exibir a imagem com a máscara.

Um exemplo interessante do uso de uma máscara animada é este abaixo. Clique no canvas para a ver a animação.

Você pode executar o exemplo acima a partir desta página, ou ver um exemplo mais sofisticado nesta página.

{lang: 'pt-BR'}
  • Felipe rodrigues

    Muito bom o exemplo, mas como eu faria caso queira fatiar o circulo, e deixar a imagem nessa fatia ?