Efeitos tipográficos no canvas

Meu contexto

<canvas> entrou na minha vida 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 app da Web <canvas>, a Esfera de cores (2007). Isso me levou ao mundo das cores e das primitivas gráficas, inspirando a criação do Sketchpad (2007-2008) para criar um aplicativo "melhor que o Paint" no navegador. Esses experimentos acabaram levando à criação da startup Mugtug com meu amigo de longa data Charles Pritchard. Estamos desenvolvendo o Darkroom em <canvas> do HTML5. O Darkroom é um app de compartilhamento de fotos não destrutivo que combina o poder dos filtros baseados em pixels com tipografia e desenhos vetoriais.

Introdução

Gráfico do banner de tela.

Com a <canvas>, os programadores de JavaScript têm controle total das cores, vetores e pixels da tela (a composição visual do monitor).

Os exemplos a seguir tratam de uma área em <canvas> que não recebeu muita atenção: a criação de efeitos de texto. A variedade de efeitos de texto que podem ser criados em <canvas> é vasta. Essas demonstrações abrangem uma subseção do que é possível. Embora estejamos lidando com "texto" neste artigo, os métodos podem ser aplicados a qualquer objeto vetorial, criando recursos visuais incríveis em jogos e outros aplicativos:

Sombras de texto em <canvas>.
Efeitos de texto semelhantes a CSS em <canvas>, criando máscaras de recorte, encontrando métricas em <canvas> e usando a propriedade sombra.
Arco-íris neon, reflexão de zebra - efeitos de encadeamento.
Efeitos de texto semelhantes ao Photoshop em exemplos de <canvas> de como usar globalCompositeOperation, createLinearGradient e createPattern.
Sombras internas e externas em <canvas>
Revelando um recurso pouco conhecido: usar o sentido horário e anti-horário para criar o inverso de uma sombra projetada (a sombra interna).
Spaceage: efeito generativo.
Efeito de texto baseado em geração em <canvas> usando o ciclo de cores hsl() e window.requestAnimationFrame para criar a sensação de movimento.

Sombras de texto no canvas

Uma das minhas adições favoritas às especificações do CSS3 (junto com border-radius, gradientes da Web e outras) é a capacidade de criar sombras. É importante perceber as diferenças entre as sombras CSS e <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.

<canvas> tem um tipo de sombra; ele é 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 em <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; // número inteiro
Distância horizontal da sombra em relação ao texto.
ctx.shadowOffsetY = 0; // número inteiro
Distância vertical da sombra em relação ao texto.
ctx.shadowBlur = 10; // número inteiro
Efeito de desfoque na sombra: quanto maior o valor, maior o desfoque.

Para começar, vamos ver como <canvas> pode emular efeitos CSS. A pesquisa de "css text-shadow" nas imagens do Google levou a algumas ótimas demonstrações para emularmos: Line25, Stereoscopic e Shadow 3D.

Gráfico 3D do CSS

O efeito 3D estereoscópico (consulte Imagem anaglífica para mais informações) é um exemplo de uma linha simples de código, usada de maneira eficaz. Com a seguinte linha de CSS, é possível criar a ilusão de profundidade quando visualizado com óculos 3D vermelho/ciano (aqueles que você recebe em filmes 3D):

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

Há duas coisas a serem observadas ao converter essa string em <canvas>:

  1. Não há shadow-blur (o terceiro valor), então não há motivo para executar a sombra, já que o 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);</pre>
  1. O EM não tem suporte em <canvas>, então ele precisa ser convertido em PX. Podemos encontrar a taxa de conversão para converter entre PT, PC, EM, EX, PX e assim por diante criando um elemento com as mesmas propriedades de fonte no DOM e definindo a largura no formato a ser medido. Ou então, para capturar a conversão EM -> PX, medimos o elemento do DOM com uma "altura: 1em".
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;</pre>

Como evitar a multiplicação alfa

Em um exemplo mais complexo, como o efeito neon encontrado em Line25, a propriedade shadowBlur precisa ser usada para emular o efeito corretamente. Como o efeito de néon depende de várias sombras, encontramos um problema: em <canvas>, cada objeto vetorial só pode ter uma sombra. Portanto, para desenhar várias sombras, você precisa desenhar várias versões do texto sobre ele mesmo. Isso resulta em multiplicação alfa e, por fim, em bordas irregulares.

Gráfico neon

Tentei executar ctx.fillStyle = "rgba(0,0,0,0)" ou "transparent" para ocultar o texto enquanto exibe a sombra. No entanto, essa tentativa foi inútil. Como a sombra é uma multiplicação do alfa fillStyle, a sombra nunca pode ser mais opaca que fillStyle.

Felizmente, há uma maneira de contornar isso: podemos desenhar o deslocamento da sombra do texto, mantendo-os separados (para que não se sobreponham) e, assim, 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);

Recorte em torno de um bloco de texto

Para limpar um pouco isso, podemos impedir que fillText seja desenhado em primeiro lugar (permitindo que a sombra seja desenhada) adicionando um caminho de recorte. Para criar um caminho de recorte em volta do texto, precisamos saber a altura do texto (chamada de "altura-em", historicamente a altura da letra "M" em uma prensa de impressão) e a largura do texto. Podemos conseguir a largura usando ctx.measureText().width, mas ctx.measureText().height não existe.

Felizmente, com o hack-ardry do CSS (confira Métricas tipográficas para mais maneiras de corrigir implementações mais antigas de <canvas> usando medições do CSS), é possível 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 para usar como um caminho de recorte, envolvendo a "sombra" e removendo a forma fictícia.

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

Unindo tudo e otimizando conforme avançamos. Se uma sombra não tem desfoque, fillText pode ser usado para o mesmo efeito, evitando que configure a máscara de recorte:

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 vai querer inserir todos esses comandos <canvas> manualmente, incluí um analisador de texto-sombra simples na fonte de demonstração. Dessa forma, você pode fornecer comandos CSS e gerar comandos <canvas>. Agora, nossos elementos <canvas> têm uma variedade de estilos que podem ser vinculados. Esses mesmos efeitos de sombra podem ser usados em qualquer objeto vetorial, de WebFonts a formas complexas importadas de SVGs, formas vetoriais generativas e assim por diante.

Sombra do texto em efeitos de tela

Intermissão (tangente ao envio de pixels)

Ao escrever esta seção do artigo, o exemplo estereoscópico me deixou curioso. Qual seria o nível de dificuldade para criar um efeito de tela de filme 3D usando <canvas> e duas imagens tiradas de perspectivas ligeiramente diferentes? Aparentemente, não muito. 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;

Agora, alguém só precisa colocar dois iPhones na testa, clicar em "gravar vídeo" ao mesmo tempo, e podemos fazer nossos próprios filmes 3D em HTML5. Alguém se voluntaria?

Óculos 3D

Reflexo de zebra e arco-íris em néon: efeitos em cadeia

Encadear vários efeitos em <canvas> pode ser simples, mas é necessário ter um conhecimento básico da globalCompositeOperation (GCO). Para comparar as operações com o GIMP (ou Photoshop): há 12 GCOs em <canvas> mais escuro, e mais claro pode ser considerado como modos de mesclagem de camadas. As outras 10 operações são aplicadas às camadas como máscaras alfa (uma camada remove os pixels da outra camada). A globalCompositeOperation vincula "camadas" (ou, no nosso caso, strings de código) e as combina de maneiras novas e interessantes:

Encadeamento de efeitos gráficos

O gráfico globalCompositeOperation mostra os modos de GCO em ação. Esse gráfico usa uma grande parte do espectro de cores e vários níveis de transparência alfa para ver com detalhes o que esperar. Recomendamos consultar a referência globalCompositeOperation da Mozilla para descrições textuais. Para mais pesquisas, saiba como a operação funciona em Composição de imagens digitais (em inglês) de Porter Duff.

Meu modo favorito é globalCompositeOperation="lighter". O Lighter mistura os pixels anexados de forma semelhante à mistura de luz. Quando a luz vermelha, verde e branca está em intensidade total, vemos a luz branca. É um recurso interessante para brincar, especialmente quando o <canvas> é definido como uma globalAlpha baixa, permitindo um controle mais preciso e bordas mais suaves. Lighter tem sido usado de muitas formas. Meu favorito recente é um criador de plano de fundo HTML5 para computador, disponível em http://weavesilk.com/ (link em inglês). Uma das minhas demonstrações, Breathing Galaxies (JS1k), também usa o modo mais leve, desenhando padrões com base nesses dois exemplos para mostrar o efeito desse modo.

Processamento de globalCompositeOperation no navegador.

Efeito de jitter de arco-íris neon

Na demonstração a seguir, vamos criar um efeito semelhante ao do Photoshop com um contorno irregular, encadeando efeitos usando a globalCompositeOperation (source-in, lighter e darker). Esta demonstração é uma progressão da demonstração "Text-Shadows in <canvas>", que usa a mesma estratégia para separar a sombra do texto (consulte a seção anterior):

Jitter de arco-íris
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 da zebra

O efeito de reflexão de zebra foi inspirado no excelente recurso do WebDesignerWall sobre como dar um toque especial à sua página com CSS. Isso leva a ideia um pouco mais longe, criando uma "reflexão" para o texto, como o que você pode encontrar no iTunes. O efeito combina fillColor (branco), createPattern (zebra.png) e linearGradient (brilho). Isso ilustra a capacidade de aplicar vários tipos de preenchimento a cada objeto vetorial:

Efeito 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 de <canvas> não abordam o assunto de sombras "internas" versus "externas". Na primeira aparição, você pode esperar que a sombra "interna" não tenha suporte. Esse não é o caso. É um pouco complicado ativar ;) Conforme proposto em uma postagem recente do F1LT3R (link em inglês), você pode criar sombras internas usando as propriedades exclusivas de regras de rotação no sentido horário e anti-horário. Para fazer isso, crie uma "sombra interna" desenhando o retângulo de contêiner e, usando regras de enrolamento opostas, desenhe uma forma de recorte, criando o inverso da forma.

O exemplo a seguir permite que a sombra interna e o fillStyle sejam estilizados com cor+gradiente+padrão simultaneamente. É possível especificar a rotação do padrão individualmente. Observe que as listras zebradas agora são perpendiculares umas às outras. Uma máscara de corte do tamanho da caixa delimitadora é usada para eliminar a necessidade de um contêiner supergrande para conter a forma de corte, melhorando a velocidade ao impedir que as partes desnecessárias da sombra sejam processadas.

Sombras internas/externas
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();
};

Com esses exemplos, você pode usar a globalCompositeOperation para encadear efeitos e produzir efeitos mais elaborados (usando mascaramento e mesclagem). A tela é sua ;)

Spaceage: efeitos generativos

Em <canvas>, passando do caractere Unicode 0x2708:

Gfaphic Unicode

…para este exemplo sombreado:

Exemplo sombreado

Isso pode ser feito por várias chamadas para ctx.strokeText() com uma lineWidth fina (0,25), enquanto diminui lentamente o deslocamento x e o alfa, proporcionando aos elementos vetoriais a sensação de movimento.

Ao mapear a posição XY dos elementos para uma onda de seno/cosseno e percorrer as cores usando a propriedade HSL, podemos criar efeitos mais interessantes, como este exemplo de "perigo biológico":

Efeito de ciclismo HSL

HSL: matiz, saturação e brilho (1978)

O HSL é um formato recém-suportado nas especificações do CSS3. Enquanto o HEX foi projetado para computadores, o HSL foi projetado para ser legível por humanos.

Para ilustrar a facilidade do HSL, para percorrer o espectro de cores, basta incrementar o "matiz" de 360. O matiz é mapeado para o espectro de forma cilíndrica. A luminosidade controla a intensidade da cor. 0% indica um pixel preto, enquanto 100% indica um pixel branco. A saturação controla o brilho ou a vivacidade de uma cor. Os cinzas são criados com uma saturação de 0%, e as cores vivas são criadas usando um valor de 100%.

Gráfico HSL

Como HSL é um padrão recente, talvez você queira continuar oferecendo suporte a navegadores mais antigos, o que é possível por meio da conversão de espaço de cores. 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 }. Em seguida, é possível usar esses valores para criar a string RGB ou RGBA. Para mais informações, consulte o artigo da Wikipédia sobre HSL (em inglês).

// 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
};
};

Como criar animações com requestAnimationFrame

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

window.requestAnimationFrame, é o novo padrão para substituir ambos, economizando energia elétrica (e alguns batimentos cardíacos do computador) ao permitir que o navegador regule animações com base nos recursos disponíveis. Alguns recursos importantes incluem:

  • Quando um usuário existe o frame, a animação pode ficar mais lenta ou ser interrompida completamente para evitar o uso de recursos desnecessários.
  • A taxa de quadros tem um limite de 60 QPS. O motivo é que ele está bem acima do nível que os humanos conseguem perceber. A maioria das pessoas, com 30 QPS, considera a animação "fluida".

No momento da redação deste artigo, é necessário usar prefixos específicos para usar requestAnimationFrame. Paul Irish criou uma camada de shim com suporte a vários fornecedores, em requestAnimationFrame para animações inteligentes:

// 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);
        };
})();

Indo um pouco mais longe, os mais ambiciosos podem vincular isso a um poly-fill, como requestAnimationFrame.js, que tem alguns recursos a serem trabalhados, que ofereceriam suporte a navegadores mais antigos ao mudar para esse novo padrã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);
})();
Gráfico de desfoque das notas
Gráfico de animação
Gráfico de matriz

Código-fonte

Com o suporte de todos os fornecedores de navegadores, não há dúvidas sobre o futuro de <canvas>. Ele pode ser transferido para os executáveis do iPhone/Android/computador usando o PhoneGap ou

Titânio (link em inglês).