Efeitos tipográficos em canvas

HTML5 Rocks

Meu plano de fundo

Conheci o <canvas> em 2006 quando o Firefox v2.0 foi lançado. Um artigo sobre Ajaxian, que descreve a matriz de transformação, me inspirou a criar meu primeiro aplicativo da web <canvas>; Color Sphere (2007).  Isso me aprofundou no mundo das cores e dos elementos gráficos básicos, inspirando a criação do Sketchpad (2007-2008) em uma tentativa de reunir um aplicativo “melhor que o Paint” no navegador.

Essas experiências acabaram levando à criação do Mugtug de inicialização com meu amigo de longa data Charles Pritchard.  Estamos desenvolvendo o Darkroom no elemento <canvas> do HTML5.  Darkroom é um aplicativo de compartilhamento de fotos não destrutivo que combina o poder dos filtros baseados em pixel com tipografia e desenhos baseados em vetor.

Introdução

pastedGraphic.png

O <canvas> traz aos programadores de JavaScript controle total de cores, vetores e pixels na tela - o layout visual do monitor.

Os exemplos a seguir tratam de outra área do <canvas> que não chama tanta atenção,  a criação de efeitos de texto.  A variedade de efeitos de texto que podem ser criados no <canvas> é tão vasta quanto você possa imaginar; essas demonstrações abrangem um subconjunto do que é possível.  Embora estejamos falando de “texto” neste artigo, os métodos podem ser aplicados a qualquer objeto vetorial, criando visuais incríveis em jogos e outros aplicativos:

Sombras de texto no <canvas>.
Efeitos de texto como CSS do <canvas> criação de máscaras de corte, localização de métricas no <canvas> e uso da propriedade de sombra.
Arco-íris neon, reflexão de zebra - efeitos de encadeamento.
Efeitos de texto como Photoshop em exemplos do <canvas> de como usar globalCompositeOperation, createLinearGradient e createPattern. 
Sombras internas e externas no <canvas>
Como revelar um recurso pouco conhecido; uso de movimento no sentido horário x anti-horário para criar o inverso de uma sombra oblíqua (a sombra interna).
Efeito de geração de espaço.
Efeito de texto baseado em geração do <canvas> usando o ciclo de cores hsl() e window.requestAnimationFrame para criar a sensação de movimento.

Sombras de texto no cCanvas

Uma de minhas adições favoritas às especificações do CSS3 (junto com raio de borda, gradientes da web e outros) é a possibilidade de criar sombras. É importante notar as diferenças entre as sombras do CSS e do <canvas>, especificamente:

O CSS usa dois métodos: box-shadow para elementos de caixa, como div, span e assim por diante, e text-shadow para conteúdo de texto.

O <canvas> tem um único tipo de sombra, usado para todos os objetos vetoriais; ctx.moveTo, ctx.lineTo, ctx.bezierCurveTo, ctx.quadradicCurveTo, ctx.arc, ctx.rect, ctx.fillText, ctx.strokeText e assim por diante. Para criar uma sombra no <canvas>, toque nestas quatro propriedades:

ctx.shadowColor = “red” // string
Cor da sombra;  RGB, RGBA, HSL, HEX e outras entradas são válidas.
ctx.shadowOffsetX = 0; // integer
Distância horizontal da sombra em relação ao texto.
ctx.shadowOffsetY = 0; // integer
Distância vertical da sombra em relação ao texto.
ctx.shadowBlur = 10; // integer
Efeito de mancha da sombra; quanto maior o valor, maior a mancha.

Para começar, vamos ver como o <canvas> pode simular os efeitos de CSS. Pesquisar nas imagens do Google por “css text-shadow” resultou em algumas demonstrações excelentes para simulação; Line25, Stereoscopic e Shadow 3D.

pastedGraphic_1.png

O efeito 3D estereoscópico (consulte Imagem de anaglifo para obter mais informações) é um exemplo de uma linha de código simples usada de modo adequado. Com a seguinte linha do CSS, podemos criar a ilusão de profundidade quando visualizada com óculos 3D vermelho/cinza (o mesmo usado em filmes 3D):

text-shadow: -0.06em 0 0 red, 0.06em 0 0 cyan;

Duas coisas devem ser consideradas ao converter essa string em <canvas>:

(1) Não há nenhuma mancha de sombra (o terceiro valor), ou seja, não há nenhum motivo para executar a sombra, pois fillText criaria os mesmos resultados:

var text = “Hello world!”
ctx.fillStyle = “#000”
ctx.fillText(text, -7, 0);
ctx.fillStyle = “red”
ctx.fillText(text, 0, 0);
ctx.fillStyle = “cyan”
ctx.fillText(text, 7, 0);

(2) Os EMs não são compatíveis com o <canvas>, de modo que precisarão ser convertidos em PXs. Podemos encontrar a taxa de conversão entre PT, PC, EM, EX, PX e assim por diante criando um elemento com as mesmas propriedades de fonte em DOM e configurando a largura de acordo com o formato a ser medido; por exemplo, para capturar a conversão EM -> PX, medimos o elemento DOM com “height: 1em”. O offsetHeight resultante seria a quantidade de PXs presentes em cada EM.

var font = “20px sans-serif”
var d = document.createElement(”span”);
d.style.cssText = “font: “ + font + “ height: 1em; display: block”
// the value to multiply PX’s by to convert to EM’s
var EM2PX = 1 / d.offsetHeight;

Como evitar a multiplicação alfa

Em um exemplo mais complexo, como, por exemplo, o efeito de neon encontrado em Line25, a propriedade shadowBlur deve ser usada para simular o efeito corretamente. Como o efeito de neon depende de várias sombras, há um problema. No <canvas>, cada objeto vetorial pode ter somente uma sombra. Assim, para desenhar várias sombras, é necessário desenhar várias versões do texto em cima dele mesmo. Isso resulta na multiplicação alfa e, por fim, em bordas irregulares.

pastedGraphic_2.png

Tentei executar ctx.fillStyle = “rgba(0,0,0,0)” ou “transparent” para ocultar o texto enquanto a sombra estava sendo exibida. No entanto, essa tentativa foi em vão, pois a sombra é uma multiplicação de alfa fillStyle e nunca pode ser mais opaca que fillStyle.

Felizmente, há uma solução. Podemos desenhar o desvio da sombra a partir do texto, mantendo-os separado (para não haver sobreposição) e, portanto, ocultando o texto na lateral da tela:

var text = “Hello world!”
var blur = 10;
var width = ctx.measureText(text).width + blur * 2;
ctx.textBaseline = “top”
ctx.shadowColor = “#000”
ctx.shadowOffsetX = width;
ctx.shadowOffsetY = 0;
ctx.shadowBlur = blur;
ctx.fillText(text, -width, 0);

Corte ao redor de um bloco de texto

Para organizar um pouco as coisas, podemos impedir que fillText seja desenhado em primeiro plano (mas permitir que a sombra seja desenhada) adicionando um caminho de corte. Para criar um caminho de corte ao redor do texto, precisamos saber a altura do texto (chamada de “em-height”; historicamente, a altura da letra “M” em uma impressão) e a largura do texto. Podemos obter a largura usando ctx.measureText().width. No entanto, ctx.measureText().height não existe.

Felizmente, por meio de especificações do CSS ( consulteMétricas tipográficas para obter mais maneiras de corrigir implementações mais antigas de <canvas> usando medidas de CSS), podemos encontrar a altura do texto medindo o offsetHeight de um <span> com as mesmas propriedades de fonte:

var d = document.createElement(”span”);
d.font = “20px arial”
d.textContent = “Hello world!”
var emHeight = d.offsetHeight;

A partir daí, podemos criar um retângulo a ser usado como um caminho de corte, envolvendo a “sombra” e removendo a forma fictícia.

ctx.rect(0, 0, width, emHeight);
ctx.clip();

Juntando tudo e fazendo otimizações progressivas, se uma sombra não tiver mancha, fillText poderá ser usado para o mesmo efeito, nos livrando de configurar a máscara de corte:

var width = ctx.measureText(text).width;
var style = shadowStyles[text];
// add a background to the current effect
ctx.fillStyle = style.background;
ctx.fillRect(0, offsetY, ctx.canvas.width, textHeight - 1)
// parse text-shadows from css
var shadows = parseShadow(style.shadow);
// loop through the shadow collection
var n = shadows.length; while(n--) {
    var shadow = shadows[n];
    var totalWidth = width + shadow.blur * 2;
    ctx.save();
    ctx.beginPath();
    ctx.rect(offsetX - shadow.blur, offsetY, offsetX + totalWidth, textHeight);
    ctx.clip();
    if (shadow.blur) { // just run shadow (clip text)
        ctx.shadowColor = shadow.color;
        ctx.shadowOffsetX = shadow.x + totalWidth;
        ctx.shadowOffsetY = shadow.y;
        ctx.shadowBlur = shadow.blur;
        ctx.fillText(text, -totalWidth + offsetX, offsetY + metrics.top);
    } else { // just run pseudo-shadow
        ctx.fillStyle = shadow.color;
        ctx.fillText(text, offsetX + (shadow.x||0), offsetY - (shadow.y||0) + metrics.top);
    }
    ctx.restore();
}
// drawing the text in the foreground
if (style.color) {
    ctx.fillStyle = style.color;
    ctx.fillText(text, offsetX, offsetY + metrics.top);
}
// jump to next em-line
ctx.translate(0, textHeight);
    

Como você não irá inserir todos esses comandos do <canvas> manualmente, incluí um analisador de sombra de texto simples na origem da demonstração. Desse modo, é possível gerar feeds de comandos do CSS e fazê-lo gerar comandos de <canvas>. Agora, nossos elementos <canvas> têm uma variedade completa de estilos a serem associados. Esses mesmos efeitos de sombra podem ser usados em qualquer objeto vetorial, desde WebFonts até formas complexas importadas de SVGs, formas vetoriais generativas e assim por diante.

Visualização Sombras de texto em efeitos do <canvas>.

pastedGraphic_3.png

Intermissão (tangente ao envio de pixels)

Ao escrever esta seção do artigo, o exemplo estereoscópico me deixou curioso.  Até que ponto seria difícil criar um efeito de tela de cinema 3D usando o <canvas> e duas imagens criadas a partir de perspectivas ligeiramente diferentes?  Aparentemente, não seria muito difícil.  O kernel a seguir combina o canal vermelho da primeira imagem (data) com o canal ciano da segunda imagem (data2):

data[i] = data[i] * 255 / 0xFF;
data[i+1] = 255 * data2[i+1] / 0xFF;
data[i+2] = 255 * data2[i+2] / 0xFF;

Consulte a Demonstração estereoscópica para ver como criar imagens a serem aprimoradas com óculos 3D (ciano/magenta).  Agora, alguém só precisa conectar dois iPhones ao fone de ouvido, clicar em “gravar vídeo” ao mesmo tempo e poderíamos fazer nossos próprios filmes 3D em HTML5.  Algum voluntário?

pastedGraphic_5.png
Óculos 3D

Arco-íris, reflexão de zebra - efeitos de encadeamento.

Os vários efeitos de encadeamento do <canvas> podem ser simples, mas é necessário ter um conhecimento básico de globalCompositeOperation (GCO). Para comparar as operações com o GIMP (ou Photoshop): existem 12 GCOs no <canvas>. darker e lighter podem ser considerados modos de mistura de camada; as outras dez operações são aplicadas às camadas como máscaras alfa (uma camada remove os pixels da outra camada). O globalCompositeOperation associa as “camadas” (ou, em nosso caso, strings de código), combinando-as de maneiras novas e interessantes:

pastedGraphic_6.png

O gráfico de globalCompositeOperation mostra os modos de GCO em funcionamento. Esse gráfico usa uma grande parte do espectro de cores e vários níveis de transparência alfa para ver em detalhes o que se deve esperar. Recomendo consultar a referência de globalCompositeOperation do Mozilla para obter descrições textuais. Para pesquisas adicionais, você pode saber como a operação funciona consultando Composição de Imagens Digitais de Porter Duff.

Meu modo favorito é globalCompositeOperation=”lighter”.  Lighter mistura os pixels anexados de modo similar a light; quando vermelho, verde e branco estão com intensidade máxima, vemos white-light. É um recurso interessante de se usar, especialmente quando o <canvas> está definido com um valor baixo de globalAlpha, permitindo controle mais refinado e bordas mais suavizadas. Lighter tem sido usado de muitas maneiras. Meu favorito recente é um criador de plano de fundo de área de trabalho HTML5 disponível em http://weavesilk.com/. Uma de minhas demonstrações, Breathing Galaxies (JS1k), também usa o modo lighter. Desenhando padrões a partir desses dois exemplos, você começa a ver o efeito desse modo.

OBSERVAÇÃO: não há suporte para alguns modos de GCO nos navegadores que precisam ser retirados. Seis modos funcionam nos navegadores (Chrome, Safari, Firefox e Opera): source-over, source-atop, destination-over, destination-out, lighter e xor. Felizmente, isso será removido nas próximas versões. Para obter mais informações, consulte Manuseio do navegador globalCompositeOperation.

Efeito de instabilidade de arco-íris neon

Na demonstração a seguir, vamos obter um brilho de arco-íris neon como Photoshop com um contorno instável, encadeando efeitos com o uso de globalCompositeOperation (source-in, lighter e darker). Essa demonstração é uma progressão da demonstração “Sombras de texto no <canvas>”, que usa a mesma estratégia para separar a sombra do texto (consulte a seção anterior):

pastedGraphic_7.png

Visualização Efeito de instabilidade de arco-íris neon.

function neonLightEffect() {
  var text = "alert('"+String.fromCharCode(0x2665)+"')";
  var font = "120px Futura, Helvetica, sans-serif";
  var jitter = 25; // the distance of the maximum jitter
  var offsetX = 30;
  var offsetY = 70;
  var blur = getBlurValue(100);
  // save state
  ctx.save();
  ctx.font = font;
  // calculate width + height of text-block
  var metrics = getMetrics(text, font);
  // create clipping mask around text-effect
  ctx.rect(offsetX - blur/2, offsetY - blur/2,
           offsetX + metrics.width + blur, metrics.height + blur);
  ctx.clip();
  // create shadow-blur to mask rainbow onto (since shadowColor doesn't accept gradients)
  ctx.save();
  ctx.fillStyle = "#fff";
  ctx.shadowColor = "rgba(0,0,0,1)";
  ctx.shadowOffsetX = metrics.width + blur;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = blur;
  ctx.fillText(text, -metrics.width + offsetX - blur, offsetY + metrics.top);
  ctx.restore();
  // create the rainbow linear-gradient
  var gradient = ctx.createLinearGradient(0, 0, metrics.width, 0);
  gradient.addColorStop(0, "rgba(255, 0, 0, 1)");
  gradient.addColorStop(0.15, "rgba(255, 255, 0, 1)");
  gradient.addColorStop(0.3, "rgba(0, 255, 0, 1)");
  gradient.addColorStop(0.5, "rgba(0, 255, 255, 1)");
  gradient.addColorStop(0.65, "rgba(0, 0, 255, 1)");
  gradient.addColorStop(0.8, "rgba(255, 0, 255, 1)");
  gradient.addColorStop(1, "rgba(255, 0, 0, 1)");
  // change composite so source is applied within the shadow-blur
  ctx.globalCompositeOperation = "source-atop";
  // apply gradient to shadow-blur
  ctx.fillStyle = gradient;
  ctx.fillRect(offsetX - jitter/2, offsetY,
               metrics.width + offsetX, metrics.height + offsetY);
  // change composite to mix as light
  ctx.globalCompositeOperation = "lighter";
  // multiply the layer
  ctx.globalAlpha = 0.7
  ctx.drawImage(ctx.canvas, 0, 0);
  ctx.drawImage(ctx.canvas, 0, 0);
  ctx.globalAlpha = 1
  // draw white-text ontop of glow
  ctx.fillStyle = "rgba(255,255,255,0.95)";
  ctx.fillText(text, offsetX, offsetY + metrics.top);
  // created jittered stroke
  ctx.lineWidth = 0.80;
  ctx.strokeStyle = "rgba(255,255,255,0.25)";
  var i = 10; while(i--) { 
      var left = jitter / 2 - Math.random() * jitter;
      var top = jitter / 2 - Math.random() * jitter;
      ctx.strokeText(text, left + offsetX, top + offsetY + metrics.top);
  }    
  ctx.strokeStyle = "rgba(0,0,0,0.20)";
  ctx.strokeText(text, offsetX, offsetY + metrics.top);
  ctx.restore();
};

Efeito de reflexão de zebra

O efeito de reflexão de zebra foi inspirado pelo excelente recurso do WebDesignerWall sobre como dividir a página com CSS. Isso leva a ideia um pouco mais além, criando uma “reflexão” para o texto, como o que você pode ver no iTunes. O efeito combina fillColor (branco), createPattern (zebra.png) e linearGradient (brilhante); isso ilustra a possibilidade de aplicar vários tipos de preenchimento a cada objeto vetorial:

pastedGraphic_8.png

Visualização Efeito de reflexão de zebra.

function sleekZebraEffect() {
  // inspired by - http://www.webdesignerwall.com/demo/css-gradient-text/
  var text = "Sleek Zebra...";
  var font = "100px Futura, Helvetica, sans-serif";

  // save state
  ctx.save();
  ctx.font = font;

  // getMetrics calculates:
  // width + height of text-block
  // top + middle + bottom baseline
  var metrics = getMetrics(text, font);
  var offsetRefectionY = -20;
  var offsetY = 70;
  var offsetX = 60;

  // throwing a linear-gradient in to shine up the text
  var gradient = ctx.createLinearGradient(0, offsetY, 0, metrics.height + offsetY);
  gradient.addColorStop(0.1, '#000');
  gradient.addColorStop(0.35, '#fff');
  gradient.addColorStop(0.65, '#fff');
  gradient.addColorStop(1.0, '#000');
  ctx.fillStyle = gradient
  ctx.fillText(text, offsetX, offsetY + metrics.top);

  // draw reflected text
  ctx.save();
  ctx.globalCompositeOperation = "source-over";
  ctx.translate(0, metrics.height + offsetRefectionY)
  ctx.scale(1, -1);
  ctx.font = font;
  ctx.fillStyle = "#fff";
  ctx.fillText(text, offsetX, -metrics.height - offsetY + metrics.top);
  ctx.scale(1, -1);

  // cut the gradient out of the reflected text 
  ctx.globalCompositeOperation = "destination-out";
  var gradient = ctx.createLinearGradient(0, offsetY, 0, metrics.height + offsetY);
  gradient.addColorStop(0.0, 'rgba(0,0,0,0.65)');
  gradient.addColorStop(1.0, '#000');
  ctx.fillStyle = gradient;
  ctx.fillRect(offsetX, offsetY, metrics.width, metrics.height);

  // restore back to original transform state
  ctx.restore();

  // using source-atop to allow the transparent .png to show through to the gradient
  ctx.globalCompositeOperation = "source-atop";

  // creating pattern from <image> sourced.
  ctx.fillStyle = ctx.createPattern(image, 'repeat');

  // fill the height of two em-boxes, to encompass both normal and reflected state
  ctx.fillRect(offsetX, offsetY, metrics.width, metrics.height * 2);
  ctx.restore();
};

Sombras internas/externas no Canvas

As especificações do <canvas> não tocam no assunto de sombras “internas” x “externas”. Na realidade, a princípio, você talvez pense que a sombra “interna” não é permitida. Esse não é o caso. A questão é um pouco mais complicada ;) Conforme proposto em uma postagem recente de F1LT3R, você pode criar sombras internas usando as propriedades exclusivas das regras de movimentação no sentido horário x anti-horário. Para fazer isso, crie uma “sombra interna” desenhando o retângulo do contêiner e, em seguida, usando as regras de movimentação oposta, desenhe uma forma de recorte, criando o inverso da forma.

O exemplo a seguir permite que borda interna e fillStyle sejam estilizados com cor+gradiente+padrão simultaneamente. É possível especificar a rotação do padrão individualmente; observe que as listras de zebra agora estão perpendiculares entre si. Uma máscara de corte do tamanho da caixa delimitadora é usada, eliminando a necessidade de um recipiente super grande para envolver a forma de corte e, assim, aumentando a velocidade ao impedir o processamento de partes desnecessárias da sombra.

pastedGraphic_9.png

View Efeito de sombra interna.

function innerShadow() {

  function drawShape() { // draw anti-clockwise
    ctx.arc(0, 0, 100, 0, Math.PI * 2, true); // Outer circle
    ctx.moveTo(70, 0);
    ctx.arc(0, 0, 70, 0, Math.PI, false); // Mouth
    ctx.moveTo(-20, -20);
    ctx.arc(30, -30, 10, 0, Math.PI * 2, false); // Left eye
    ctx.moveTo(140, 70);
    ctx.arc(-20, -30, 10, 0, Math.PI * 2, false); // Right eye
  };

  var width = 200;
  var offset = width + 50;
  var innerColor = "rgba(0,0,0,1)";
  var outerColor = "rgba(0,0,0,1)";

  ctx.translate(150, 170);

  // apply inner-shadow
  ctx.save();
  ctx.fillStyle = "#000";
  ctx.shadowColor = innerColor;
  ctx.shadowBlur = getBlurValue(120);
  ctx.shadowOffsetX = -15;
  ctx.shadowOffsetY = 15;

  // create clipping path (around blur + shape, preventing outer-rect blurring)
  ctx.beginPath();
  ctx.rect(-offset/2, -offset/2, offset, offset);
  ctx.clip();

  // apply inner-shadow (w/ clockwise vs. anti-clockwise cutout)
  ctx.beginPath();
  ctx.rect(-offset/2, -offset/2, offset, offset);
  drawShape();
  ctx.fill();
  ctx.restore();

  // cutout temporary rectangle used to create inner-shadow
  ctx.globalCompositeOperation = "destination-out";
  ctx.fill();

  // prepare vector paths
  ctx.beginPath();
  drawShape();

  // apply fill-gradient to inner-shadow
  ctx.save();
  ctx.globalCompositeOperation = "source-in";
  var gradient = ctx.createLinearGradient(-offset/2, 0, offset/2, 0);
  gradient.addColorStop(0.3, '#ff0');
  gradient.addColorStop(0.7, '#f00');
  ctx.fillStyle = gradient;
  ctx.fill();

  // apply fill-pattern to inner-shadow
  ctx.globalCompositeOperation = "source-atop";
  ctx.globalAlpha = 1;
  ctx.rotate(0.9);
  ctx.fillStyle = ctx.createPattern(image, 'repeat');
  ctx.fill();
  ctx.restore();

  // apply fill-gradient
  ctx.save();
  ctx.globalCompositeOperation = "destination-over";
  var gradient = ctx.createLinearGradient(-offset/2, -offset/2, offset/2, offset/2);
  gradient.addColorStop(0.1, '#f00');
  gradient.addColorStop(0.5, 'rgba(255,255,0,1)');
  gradient.addColorStop(1.0, '#00f');
  ctx.fillStyle = gradient
  ctx.fill();

  // apply fill-pattern
  ctx.globalCompositeOperation = "source-atop";
  ctx.globalAlpha = 0.2;
  ctx.rotate(-0.4);
  ctx.fillStyle = ctx.createPattern(image, 'repeat');
  ctx.fill();
  ctx.restore();

  // apply outer-shadow (color-only without temporary layer)
  ctx.globalCompositeOperation = "destination-over";
  ctx.shadowColor = outerColor;
  ctx.shadowBlur = 40;
  ctx.shadowOffsetX = 15;
  ctx.shadowOffsetY = 10;
  ctx.fillStyle = "#fff";
  ctx.fill();
};

A partir destes exemplos, usando globalCompositeOperation, podemos encadear efeitos para produzir efeitos mais elaborados (usando mascaramento e mistura). A tela é sua ostra ;)

Efeitos de geração de espaço.

No <canvas>, a partir do caractere unicode 0x2708:

pastedGraphic_10.png

... até este exemplo sombreado:

pastedGraphic_11.png

... pode ser obtido por várias chamadas para ctx.strokeText() com uma lineWidth fina (0,25), diminuindo lentamente o desvio de x e alfa e dando aos elementos vetoriais a sensação de movimento.

Mapeando a posição XY dos elementos para uma onda de seno/cosseno e percorrendo as cores com o uso da propriedade HSL, podemos criar efeitos mais interessantes, como este exemplo de “perigo biológico”:

pastedGraphic_12.png

HSL: Hue, Saturation, Lightness (Matiz, saturação e luminosidade) (1978)

HSL é um formato aceito recentemente nas especificações do CSS3.  Onde HEX tiver sido desenvolvido para computadores, HSL será desenvolvido para ser legível por humanos.

Ilustração da facilidade do HSL: para percorrer o espectro de cores, podemos simplesmente aumentar a “matiz” de 360; a matiz é mapeada para o espectro de modo cilíndrico. Os controles de luminosidade mostram até que ponto a cor é escura/clara; 0% indica um pixel preto e 100% indica um pixel branco. A saturação controla até que ponto a cor é brilhante ou vívida; os cinzas são criados com uma saturação de 0%, e as cores vívidas são criadas com um valor de 100%.

pastedGraphic_13.png

Como HSL é um padrão recente, você talvez queira continuar oferecendo suporte para navegadores mais antigos, o que é possível por meio da conversão do espaço de cor. O código a seguir aceita um objeto HSL {H: 360, S: 100, L: 100} e gera um objeto RGB {R: 255, G: 255, B: 255}. Aqui, você pode usar esses valores para criar a string rgb ou rgba. Para obter informações mais detalhadas, consulte o valioso artigo da Wikipedia sobre HSL.

// HSL (1978) = H: Hue / S: Saturation / L: Lightness
HSL_RGB = function (o) { // { H: 0-360, S: 0-100, L: 0-100 }
  var H = o.H / 360,
      S = o.S / 100,
      L = o.L / 100,
      R, G, B, _1, _2;

  function Hue_2_RGB(v1, v2, vH) {
    if (vH < 0) vH += 1;
    if (vH > 1) vH -= 1;
    if ((6 * vH) < 1) return v1 + (v2 - v1) * 6 * vH;
    if ((2 * vH) < 1) return v2;
    if ((3 * vH) < 2) return v1 + (v2 - v1) * ((2 / 3) - vH) * 6;
    return v1;
  }

  if (S == 0) { // HSL from 0 to 1
    R = L * 255;
    G = L * 255;
    B = L * 255;
  } else {
    if (L < 0.5) {
      _2 = L * (1 + S);
    } else {
      _2 = (L + S) - (S * L);
    }
    _1 = 2 * L - _2;

    R = 255 * Hue_2_RGB(_1, _2, H + (1 / 3));
    G = 255 * Hue_2_RGB(_1, _2, H);
    B = 255 * Hue_2_RGB(_1, _2, H - (1 / 3));
  }

  return {
    R: R,
    G: G,
    B: B
  };
};

Criação de animações com requestAnimationFrame

Antigamente, para criar animações em JavaScript, havia duas opções: setTimeout e setInterval.

window.requestAnimationFrame é o novo padrão para substituir ambos. Ele economiza a energia mundial (e alguns batimentos do computador) permitindo que o navegador regule as animações com base nos recursos disponíveis. Alguns recursos importantes incluem:

  • Quando um usuário possui o quadro, a animação pode ficar mais lenta ou ser interrompida por completo para evitar o uso de recursos desnecessários.
  • A taxa de quadros tem um limite de 60 FPS. Isso acontece porque ela está bem acima do nível que pode ser notado pelos humanos (a maioria dos humanos considera a animação “fluida” com uma taxa de 30 FPS).

No momento em que o artigo foi escrito, os prefixos específicos do fornecedor deviam usar requestAnimationFrame. Paul Irish criou uma camada estreita que tem suporte para vários fornecedores, disponível em requestAnimationFrame for smart animating:

// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       || 
          window.webkitRequestAnimationFrame || 
          window.mozRequestAnimationFrame    || 
          window.oRequestAnimationFrame      || 
          window.msRequestAnimationFrame     || 
          function(/* function */ callback, /* DOMElement */ element){
            window.setTimeout(callback, 1000 / 60);
          };
})();

Aprofundando isso um pouco mais, se a associação for mais complexa com um poly-fill como requestAnimationFrame.js (alguns recursos devem ser removidos), o suporte para navegadores mais antigos será estendido durante a mudança para esse novo padrão.

O exemplo a seguir mostra como criar uma animação, enviando milhares de chamadas para strokeText por meio de um alfa baixo sem danificar o navegador. Os controles são um pouco estranhos, mas produzem resultados interessantes:

Visualização Efeito de espaço.

(function animate() {
  var i = 50;
  while(i--) {
      if (n > endpos) return;

      n += definition;
      ctx.globalAlpha = (0.5 - (n + startpos) / endpos) * alpha;
      if (doColorCycle) {
          hue = n + color;
          ctx.strokeStyle = "hsl(" + (hue % 360) + ",99%,50%)"; // iterate hue
      }
      var x = cos(n / cosdiv) * n * cosmult; // cosine
      var y = sin(n / sindiv) * n * sinmult; // sin
      ctx.strokeText(text, x + xoffset, y + yoffset); // draw rainbow text
  }
  timeout = window.requestAnimationFrame(animate, 0);
})();
pastedGraphic_14.png
pastedGraphic_16.png
pastedGraphic_17.png

Código-fonte

Com suporte do fornecedor do navegador, não há dúvida quanto ao futuro do <canvas>. Ele pode ser transferido para executáveis do iPhone/Android/Desktop usando PhoneGap ou Titanium.

O código-fonte pode ser encontrado em CanvasTextEffects.zip

Comments

0