Efeitos tipográficos no canvas

Minha experiência

<canvas> entrou no meu radar em 2006, quando o Firefox v2.0 foi lançado. Um artigo no Ajaxian, que descreve a matriz de transformação, me inspirou a criar meu primeiro web app <canvas>: Color Sphere (2007). Isso me levou ao mundo das cores e das primitivas gráficas, inspirando a criação do Sketchpad (2007-2008) em um esforço 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 HTML5 <canvas>. O Darkroom é um app de compartilhamento de fotos não destrutivo que combina o poder dos filtros baseados em pixels com a tipografia e o desenho baseados em vetores.

Introdução

Gráfico de banner do Canvas.

O <canvas> oferece aos programadores de JavaScript controle total sobre as cores, os vetores e os pixels nas telas, ou seja, a composição visual do monitor.

Os exemplos a seguir lidam com 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 no <canvas> é tão vasta quanto você pode imaginar. Estas demonstrações abrangem uma subseção do que é possível. Embora este artigo trate de "texto", os métodos podem ser aplicados a qualquer objeto vetorial, criando visuais incríveis em jogos e outros aplicativos:

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

Sombras de texto na tela

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

<canvas> tem um tipo de sombra, que é usado para todos os objetos vetoriais: ctx.moveTo, ctx.lineTo, ctx.bezierCurveTo, ctx.quadradicCurveTo, ctx.arc, ctx.rect, ctx.fillText, ctx.strokeText etc. Para criar uma sombra em <canvas>, use estas quatro propriedades:

ctx.shadowColor = "red" // string
Cor da sombra. Entradas RGB, RGBA, HSL, HEX e outras 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 desfoque na sombra. Quanto maior o valor, maior o desfoque.

Para começar, vamos ver como o <canvas> pode emular efeitos CSS. Pesquisar "css text-shadow" nas Imagens do Google levou a algumas demonstrações excelentes para emularmos: Line25, Stereoscopic e Shadow 3D.

Gráfico 3D do CSS

O efeito 3D estereoscópico (consulte Imagem anáglifa para mais informações) é um exemplo de uma linha de código simples, mas muito útil. Com a seguinte linha de CSS, podemos criar a ilusão de profundidade quando visualizada com óculos 3D vermelhos/ciano (do tipo 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á desfoque de sombra (o terceiro valor), então não há motivo para executar a sombra, já que 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. As unidades EM não são compatíveis com o <canvas>, então precisam ser convertidas em PX. Podemos encontrar a proporção de conversão 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 para o formato a ser medido. Por exemplo, para capturar a conversão de EM para PX, mediríamos o elemento DOM com "height: 1em". O offsetHeight resultante seria o número de PXs 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;</pre>

Como evitar a multiplicação alfa

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

Gráfico neon

Tentei executar ctx.fillStyle = "rgba(0,0,0,0)" ou "transparent" para ocultar o texto e mostrar a sombra, mas não deu certo. Como a sombra é uma multiplicação do alfa do fillStyle, ela nunca pode ser mais opaca que o 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 ao redor de um bloco de texto

Para limpar um pouco, podemos impedir que o fillText seja desenhado em primeiro lugar (enquanto permitimos que a sombra seja desenhada) adicionando um caminho de recorte. Para criar um caminho de recorte ao redor 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 usar ctx.measureText().width para saber a largura, mas ctx.measureText().height não existe.

Felizmente, com um truque de CSS (consulte Métricas tipográficas para ver mais maneiras de corrigir implementações mais antigas de <canvas> usando medições 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;

Em seguida, podemos criar um retângulo para usar como caminho de corte, envolvendo a "sombra" e removendo a forma fictícia.

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

Para juntar tudo e otimizar o processo, se uma sombra não tiver desfoque, fillText poderá ser usado para o mesmo efeito, evitando que tenhamos que 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 vai querer inserir todos esses comandos <canvas> manualmente, incluí um analisador de sombra de texto simples na fonte de demonstração. Assim, você pode inserir comandos CSS e gerar comandos <canvas>. Agora, os elementos <canvas> têm uma ampla variedade de estilos que podem ser usados. 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.

Sombra de texto em efeitos de tela

Intervalo (tangente no pixel-pushing)

Ao escrever esta seção do artigo, o exemplo estereoscópico me deixou curioso. Seria muito difícil criar um efeito de tela de cinema 3D usando o <canvas> e duas imagens tiradas de perspectivas ligeiramente diferentes? Parece que não é tão 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;

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

Óculos 3D

Neon-rainbow, zebra-reflection—chaining effects

Encadear vários efeitos em <canvas> pode ser simples, mas é necessário um conhecimento básico da globalCompositeOperation (GCO). Para comparar as operações com o GIMP (ou Photoshop): há 12 GCOs em <canvas> darker, e lighter podem ser considerados 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). A globalCompositeOperation une "camadas" (ou, no nosso caso, strings de código), combinando-as de maneiras novas e interessantes:

Gráficos de efeitos de encadeamento

O gráfico globalCompositeOperation mostra os modos GCO em ação. Ele usa uma grande parte do espectro de cores e vários níveis de transparência alfa para que você possa ver em detalhes o que esperar. Recomendo consultar a referência globalCompositeOperation da Mozilla para descrições textuais. Para saber mais, confira como a operação funciona em Compositing Digital Images (link em inglês), de Porter Duff.

Meu modo favorito é globalCompositeOperation="lighter". A opção "Mais claro" mistura os pixels anexados de forma semelhante à mistura de luz. Quando as luzes vermelha, verde e branca estão em intensidade máxima, vemos uma luz branca. É um recurso interessante para testar, especialmente quando o <canvas> é definido como um globalAlpha baixo, o que permite um controle mais preciso e bordas mais suaves. O Lighter tem sido usado para muitas coisas. Meu uso favorito recente é um criador de plano de fundo para computador em HTML5, que pode ser encontrado em http://weavesilk.com/. Uma das minhas demonstrações, Breathing Galaxies (JS1k), também usa o modo mais claro. Ao desenhar padrões desses dois exemplos, você começa a ver o efeito que esse modo produz.

Processamento de globalCompositeOperation pelo navegador.

Efeito de tremulação neon com arco-íris

Na demonstração a seguir, vamos criar um brilho de arco-íris neon semelhante ao do Photoshop com um contorno tremido, encadeando efeitos usando a globalCompositeOperation (source-in, lighter e darker). Esta demonstração é uma progressão da demonstração "Sombras de texto em <canvas>", usando a mesma estratégia para separar a sombra do texto (consulte a seção anterior):

Instabilidade do 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 reflexo de zebra

O efeito Zebra Reflection foi inspirado no excelente recurso do WebDesignerWall sobre como deixar sua página mais interessante com CSS. Isso leva a ideia um pouco mais longe, criando um "reflexo" para o texto, como o que você pode ver 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 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 na tela

As especificações de <canvas> não abordam o assunto de sombras "internas" e "externas". Na verdade, à primeira vista, você pode achar que a sombra "interna" não é compatível. Esse não é o caso. É um pouco mais difícil de ativar ;) Conforme proposto em uma postagem recente de F1LT3R, é possível criar sombras internas usando as propriedades exclusivas das regras de enrolamento no sentido horário e anti-horário. Para fazer isso, crie uma "sombra interna" desenhando um retângulo de contêiner e, em seguida, usando regras de enrolamento opostas, desenhe uma forma de recorte, criando o inverso da forma.

O exemplo a seguir permite que inner-shadow 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 contêiner supergrande para incluir a forma do recorte. Isso melhora a velocidade, evitando 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 ver que, usando globalCompositeOperation, podemos encadear efeitos, produzindo efeitos mais elaborados (utilizando mascaramento e mesclagem). A tela é sua ;)

Spaceage: efeitos generativos

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

Gráfico Unicode

…para este exemplo sombreado:

Exemplo sombreado

…pode ser alcançado com várias chamadas para ctx.strokeText() com um lineWidth fino (0,25), enquanto diminui lentamente o deslocamento x e o alfa, aos elementos vetoriais a sensação de movimento.

Ao mapear a posição XY dos elementos para uma onda senoidal/cossenoide e alternar entre as cores usando a propriedade HSL, podemos criar efeitos mais interessantes, como este exemplo de "risco biológico":

Efeito de ciclo de HSL

HSL: matiz, saturação, luminosidade (1978)

O HSL é um formato recém-compatível com as especificações do CSS3. Enquanto o HEX foi projetado para computadores, o HSL é 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 maneira cilíndrica. A luminosidade controla o quão escura/clara é a 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 tons de cinza são criados com uma saturação de 0%, e as cores vivas são criadas usando um valor de 100%.

Gráfico de HSL

Como o HSL é um padrão recente, talvez você queira continuar oferecendo suporte a navegadores mais antigos, o que é possível com a 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 }. A partir daí, você pode usar esses valores para criar sua string rgb ou rgba. Para mais informações, consulte o artigo da Wikipédia 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
};
};

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 que substitui ambos, economizando eletricidade (e alguns batimentos cardíacos do computador) ao permitir que o navegador regule as animações com base nos recursos disponíveis. Alguns recursos importantes incluem:

  • Quando um usuário sai do frame, a animação pode desacelerar ou parar completamente para evitar o uso de recursos desnecessários.
  • Há um limite de frame rate de 60 QPS. Isso ocorre porque esse valor está bem acima do nível que os humanos podem notar (a maioria das pessoas percebe a animação como "fluida" a 30 FPS).

No momento, prefixos específicos do fornecedor são necessários para usar requestAnimationFrame. Paul Irish criou uma camada de shim com suporte a vários fornecedores em requestAnimationFrame para animação inteligente:

// 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 além, os mais ambiciosos podem vincular isso a um polyfill, como requestAnimationFrame.js (há alguns recursos a serem resolvidos) que oferecem suporte a navegadores mais antigos em maior medida, ao mesmo tempo que mudam 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 de 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 do <canvas>. Ele pode ser portado para os executáveis do iPhone/Android/computador usando o PhoneGap ou

Titanium.