Efectos tipográficos en lienzo

Mi fondo

<canvas> ingresó a mi conocimiento en 2006, cuando se lanzó Firefox v2.0. Un artículo sobre Ajaxian, en el que se describe la matriz de transformación, me inspiró a crear mi primera app web de <canvas>: Color Esfera (2007). lo que me sumergió en el mundo de los colores y las primitivas gráficas, lo que inspiró la creación de Sketchpad (2007-2008) en un esfuerzo por crear una aplicación "mejor que Paint" en el navegador. Estos experimentos llevaron a la creación de la startup Mugtug con mi amigo de mucho tiempo Charles Pritchard. Estamos desarrollando Darkroom en HTML5 <canvas>. Darkroom es una app no destructiva para compartir fotos que combina los poderes de los filtros basados en píxeles con dibujos y tipografía basados en vectores.

Introducción

Gráfico del banner de lienzo.

<canvas> brinda a los programadores de JavaScript el control total de los colores, los vectores y los píxeles en sus pantallas (la composición visual del monitor).

Los siguientes ejemplos tratan sobre un área en <canvas> que no llamó mucho la atención: la creación de efectos de texto. La variedad de efectos de texto que se pueden crear en <canvas> es tan amplia como puedas imaginar. Estas demostraciones abarcan una subsección de lo que es posible. Aunque en este artículo nos tratamos con "texto", los métodos se pueden aplicar a cualquier objeto vectorial y se pueden crear elementos visuales atractivos en juegos y otras aplicaciones:

Sombras de texto en <canvas>
Efectos de texto similares a CSS en <canvas> que crean máscaras de recorte, buscan métricas en <canvas> y usan la propiedad de sombras.
Arcoíris de neón, reflexión sobre cebras y efectos de encadenamiento.
Efectos de texto similares a Photoshop en <canvas> ejemplos del uso de globalcomposeOperation, createLinearGradient y createPattern.
Sombras interiores y exteriores en <canvas>
Revela un atributo conocido; usando el devanado en el sentido de las manecillas del reloj en lugar de uno en sentido contrario a las manecillas del reloj para crear la inversa de una sombra paralela (la sombra interior).
Spaceage: efecto generativo.
Efecto de texto basado en la generación en <canvas> con el ciclo de color hsl() y window.requestAnimationFrame para crear la sensación de movimiento.

Sombras de texto en Canvas

Una de mis incorporaciones favoritas a las especificaciones de CSS3 (junto con el radio de borde, los gradientes web y otras) es la capacidad de crear sombras. Es importante tener en cuenta las diferencias entre las sombras CSS y <canvas>, en particular las siguientes:

CSS usa dos métodos; box-shadow para elementos de cuadro, como div, span, etc., y text-shadow para contenido de texto.

<canvas> tiene un tipo de sombra: se usa para todos los objetos vectoriales: ="{.moveTo, ="{.lineTo, ="{.bezierCurveTo, ="{.quadradicCurveTo, ="{.arc, ="{.rect, ? Para crear una sombra en <canvas>, presiona estas cuatro propiedades:

ctx.shadowColor = "red" // cadena
Color de la sombra; las entradas RGB, RGBA, HSL, HEX y otras son válidas.
ctx.shadowOffsetX = 0; // número entero
Distancia horizontal de la sombra, en relación con el texto
ctx.shadowOffsetY = 0; // número entero
Distancia vertical de la sombra en relación con el texto.
ctx.shadowBlur = 10; // número entero
Con el efecto de desenfoque en la sombra, cuanto mayor sea el valor, mayor será el desenfoque.

Para comenzar, veamos cómo <canvas> puede emular efectos CSS. La búsqueda de "css text-shadow" en las imágenes de Google nos llevó a realizar algunas demostraciones excelentes para emular: Line25, Estereoscópica y Shadow 3D.

Gráfico CSS 3D

El efecto estereoscópico en 3D (consulta Imagen de anaglifo para obtener más información) es un ejemplo de una línea de código simple que se usa mucho. Con la siguiente línea de CSS, podemos crear la ilusión de profundidad cuando se ven con lentes rojos o cian en 3D (los que se usan en las películas en 3D):

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

Debes tener en cuenta dos aspectos cuando conviertas esta string en <canvas>:

  1. No hay shadow-blur (el tercer valor), por lo que no hay razón para ejecutar la sombra, ya que fillText crearía los mismos 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. Los EM no son compatibles con <canvas>, por lo que tendrán que convertirse en PX. Podemos encontrar la proporción de conversión para la conversión entre PT, PC, EM, EX, PX y otros. Para ello, creamos un elemento con las mismas propiedades de la fuente en DOM y configuramos el ancho en el formato que se medirá. Por ejemplo, para capturar la conversión EM -> PX, se mediría el elemento DOM con una altura: 1em. El desplazamiento resultante de PX en cada uno sería de la altura de PX.
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>

Evitar la multiplicación alfa

En un ejemplo más complejo, como el efecto Neon que se encuentra en Line25, se debe usar la propiedad shadowBlur para emular el efecto de forma correcta. Dado que el efecto neon se basa en varias sombras, nos encontramos con un problema. En <canvas>, cada objeto vectorial solo puede tener una sombra. Por lo tanto, para dibujar varias sombras, debes dibujar varias versiones del texto sobre sí mismo. Esto da como resultado una multiplicación alfa y, en última instancia, bordes dentados.

Gráfico de neón

Intenté ejecutar ctx.fillStyle = "rgba(0,0,0,0)" o "transparent" para ocultar el texto, mientras se mostraba la sombra, pero este intento fue inútil. Como la sombra es una multiplicación del fillStyle alfa, la sombra nunca puede ser más opaca que el fillStyle.

Afortunadamente, hay una manera de solucionar esto: podemos dibujar el desplazamiento de la sombra del texto, manteniéndolos separados (para que no se superpongan) y, por lo tanto, ocultar el texto del lado de la pantalla:

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 alrededor de un bloque de texto

Para limpiar esto un poco, podemos agregar una ruta de recorte para evitar que se dibuje el fillText en el primer lugar (y, al mismo tiempo, permitir que se dibuje la sombra). Para crear una ruta de recorte que encierre el texto, necesitamos conocer la altura del texto (llamada "em-height" históricamente, la altura de la letra "M" en una imprenta) y el ancho del texto. Podemos obtener el ancho usando ctx.measureText().width. Sin embargo, ctx.measureText().height no existe.

Afortunadamente, gracias a los trucos de CSS (consulta Métricas tipográficas para conocer más formas de corregir implementaciones anteriores de <canvas> con medidas de CSS), podemos encontrar la altura del texto midiendo el offsetHeight de un <span> con las mismas propiedades de fuente:

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

A partir de ahí, podemos crear un rectángulo para usar como ruta de recorte: encerrar la “sombra” y quitar la forma simulada.

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

Al unir todo y optimizar sobre la marcha. Si una sombra no tiene desenfoque, fillText se puede usar para obtener el mismo efecto y evitar que configuremos la 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 no querrás ingresar todos estos comandos <canvas> de forma manual, incluí un analizador de sombra de texto simple en la fuente de demostración. De esta manera, puedes proporcionarle comandos de CSS y hacer que genere comandos <canvas>. Ahora, nuestros elementos <canvas> tienen una gran variedad de estilos a los que se puede vincular. Estos mismos efectos de sombra se pueden usar en cualquier objeto vectorial, desde WebFonts hasta formas complejas importadas desde SVG hasta formas de vectores generativos, entre otros.

Sombra de texto en efectos de lienzo

Intermisión (tangente al envío de píxeles)

Al escribir esta sección del artículo, el ejemplo estereoscópico me hizo sentir curiosidad. ¿Qué tan difícil sería crear un efecto de pantalla de película en 3D con <canvas> y dos imágenes tomadas desde perspectivas ligeramente diferentes? Aparentemente, no es muy difícil. El siguiente kernel combina el canal rojo de la primera imagen (datos) con el canal cian de la segunda imagen (data2):

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

Ahora, alguien solo tiene que pegar dos iPhones con cinta adhesiva en la frente y hacer clic en "Grabar video" al mismo tiempo, y podríamos crear nuestras propias películas 3D en HTML5. ¿Hay algún voluntario?

Gafas 3D

Arcoíris de neón, reflejo de cebras: efectos de encadenamiento

Encadenar múltiples efectos en <canvas> puede ser simple, pero se requiere un conocimiento básico de globalGlobalOperation (GCO). Para comparar las operaciones con GIMP (o Photoshop), hay 12 GCO en <canvas> más oscuros y más claros se pueden considerar como modos de combinación de capas. Las otras 10 operaciones se aplican a las capas como máscaras alfa (una capa quita los píxeles de la otra). globalComposeOperation une las "capas" (o, en nuestro caso, cadenas de código) y las combina de formas nuevas y emocionantes:

Encadenamiento de gráficos de efectos

El gráfico globalGlobalOperation muestra los modos de GCO en funcionamiento. Este gráfico usa una gran parte del espectro de colores y varios niveles de transparencia alfa para ver en detalle qué esperar. Te recomendamos consultar la referencia globalGlobalOperation de Mozilla para obtener descripciones textuales. Para obtener más información, puedes obtener información sobre cómo funciona la operación en el artículo Composición de imágenes digitales de Porter Duff.

Mi modo favorito es globalGlobalOperation="lighter". La opción más clara mezcla los píxeles agregados de manera similar a como se mezcla la luz; cuando la luz roja, verde y blanca están en máxima intensidad, vemos luz blanca. Es una función emocionante para experimentar, en especial cuando <canvas> está configurado en un globalAlpha bajo, lo que permite un control más preciso y bordes más suaves. Encendedor se usa en muchos usos. Mi favorito más reciente es un creador de fondos HTML5 para computadoras de escritorio, que puedes encontrar en http://weavesilk.com/. Una de mis demostraciones, Breathing Galaxies (JS1k), también usa el modo más claro: dibuja patrones de estos dos ejemplos que puedes comenzar a ver qué efecto produce este modo.

Manejo del navegador globalcomposeOperation.

Efecto de jitter arcoíris y neón

En la siguiente demostración, lograremos un resplandor de arcoíris similar a Photoshop con un contorno irregular, ya que encadenaremos efectos mediante global compuestoOperation (source-in, más claro y más oscuro). Esta es una progresión de la demostración de "Sombras de texto en <canvas>" que usa la misma estrategia para separar la sombra del texto (consulta la sección anterior):

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

Efecto de reflexión de cebra

El efecto Zebra Reflection se inspiró en el excelente recurso de WebDesignerWall sobre cómo mejorar tu página con CSS. Esto lleva la idea un poco más allá y crea una "reflexión" para el texto, como lo que podrías ver en iTunes. El efecto combina fillColor (blanco), createPattern (zebra.png) y linearGradient (brillante); esto ilustra la capacidad de aplicar varios tipos de relleno a cada objeto vectorial:

Efecto cebra
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 interiores/exteriores en Canvas

Las especificaciones de <canvas> no tratan sobre el tema de sombras "interiores" frente a "sombras externas". De hecho, a primera vista, es posible que esperes que la sombra "interna" no sea compatible. Este no es el caso. Es un poco más complicado habilitar ;). Como se propuso en una publicación reciente de F1LT3R, puedes crear sombras internas con las propiedades únicas de las reglas de bobinado en el sentido de las manecillas del reloj en comparación con las reglas de bobinado en sentido contrario a las manecillas del reloj. Para ello, creas una "sombra interior" dibujando un rectángulo del contenedor y, luego, con reglas de bobinado opuestas, dibuja una forma de corte, lo que crea lo opuesto a la forma.

En el siguiente ejemplo, se permite que la sombra interna y el fillStyle se estilicen con color + gradiente + patrón de forma simultánea. Puedes especificar la rotación de patrones de forma individual. Observa que las rayas de cebra ahora son perpendiculares entre sí. Se usa una máscara de recorte del tamaño del cuadro delimitador, lo que elimina la necesidad de un contenedor muy grande para encerrar la forma de corte, lo que mejora la velocidad ya que evita que se procesen las partes innecesarias de la sombra.

Sombras interiores/exteriores
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 de estos ejemplos, se puede observar que, con globalCompresión, podemos encadenar efectos y producir efectos más elaborados (con el enmascaramiento y la combinación). La pantalla es tu ostra ;)

Spaceage: efectos generativos

En <canvas>, a partir del carácter Unicode 0x2708:

Gfaphic Unicode

... a este ejemplo sombreado:

Ejemplo con sombreado

Se puede lograr mediante varias llamadas a ctx.strokeText() con un lineWidth delgado (0.25), mientras que se disminuye lentamente el desplazamiento de x y el valor alfa, lo que brinda a nuestros elementos vectoriales la sensación de movimiento.

Si asignamos la posición XY de los elementos a una onda seno/coseno y recorremos los colores con la propiedad HSL, podemos crear efectos más interesantes, como este ejemplo de "riesgo biológico":

Efecto de ciclo HSL

HSL: Tono, saturación y luminosidad (1978)

HSL es un formato recientemente admitido en las especificaciones del CSS3. Cuando el formato HEX se diseñó para computadoras, HSL está diseñado para ser legible por humanos.

Para ilustrar la facilidad de HSL, para recorrer el espectro de colores, simplemente se debe incrementar el "matiz" de 360; el matiz se asigna al espectro de forma cilíndrica. La luminosidad controla qué tan oscuro/claro es el color; 0% indica un píxel negro, mientras que 100% indica un píxel blanco. La saturación controla el brillo o la intensidad de un color. Las grises se crean con una saturación del 0% y los colores intensos se crean con un valor del 100%.

Gráfico HSL

Dado que HSL es un estándar reciente, te recomendamos que sigas admitiendo navegadores más antiguos, lo cual es posible mediante la conversión del espacio de color. El siguiente código acepta un objeto HSL { H: 360, S: 100, L: 100} y genera un objeto RGB { R: 255, G: 255, B: 255 }. A partir de allí, puedes usar esos valores para crear tu cadena RGB o rgba. Para obtener información más detallada, consulta el artículo detallado de 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
};
};

Cómo crear animaciones con requestAnimationFrame

Antes, para crear animaciones en JavaScript, había dos opciones: setTimeout y setInterval.

window.requestAnimationFrame es el nuevo estándar que reemplaza ambos: le permite ahorrar electricidad al mundo (y a tu computadora unas cuantas señales) permitiendo que el navegador regule animaciones según los recursos disponibles. Estas son algunas funciones importantes:

  • Cuando un usuario existe el fotograma, la animación puede ralentizarse o detenerse por completo para evitar el uso de recursos innecesarios.
  • Existe un límite de velocidad de fotogramas de 60 FPS. El motivo es que está muy por encima del nivel que las personas pueden notar (la mayoría de las personas en 30 FPS ven que la animación es "fluida").

Al momento de escribir, se requieren prefijos específicos del proveedor para usar requestAnimationFrame. Paul irlandés creó una capa de corrección de compatibilidad que es compatible con varias líneas, en requestAnimationFrame para la animación 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);
        };
})();

Si vamos un poco más allá, los más ambiciosos podrían vincular esto con un polyfill como requestAnimationFrame.js (hay algunas funciones por resolver) que admitiría navegadores más antiguos hasta cierto punto, mientras cambiaba a este nuevo estándar.

(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 desenfoque de notas
Gráfico de animación
Gráfico de matriz

Código fuente

Con la compatibilidad de todos los proveedores del navegador, no hay dudas sobre el futuro de <canvas>, ya que se puede transferir a los ejecutables de iPhone, Android y computadoras de escritorio a través de PhoneGap.

Titanium.