lunes, 24 de abril de 2017

EFECTOS TI POG´RAFICOS EN CANVAS

En el se describía la matriz de transformación, me inspiró para crear mi primera aplicación web con <canvas>, Color Sphere (2007).  Esto me sumergió en el mundo de los colores y las primitivas gráficas, lo que me inspiró para crear Sketchpad (2007-2008) en un intento por incluir en el navegador una aplicación “mejor que Paint”.
Estos experimentos culminaron con la creación del primer Mugtug, junto a mi viejo amigo Charles Pritchard.  Estamos desarrollando Darkroom en <canvas> con HTML5.  Darkroom es una aplicación no destructiva para compartir fotos que combina la potencia de filtros basados en píxeles con el dibujo y la tipografía basados en vectores.
<canvas> ofrece a los programadores de JavaScript un control absoluto de los colores, los vectores y los píxeles en sus pantallas: la composición visual del monitor.
Los siguientes ejemplos tratan un área de <canvas> a la que no se le ha prestado mucha atención: la creación de efectos de texto.  La gran variedad de efectos de texto que se pueden crear en <canvas> es tan amplia como se podría imaginar. Estas demos cubren una subsección de lo que es posible hacer.  Aunque en este tutorial vamos a hablar sobre “texto”, los métodos se pueden aplicar a cualquier objeto vector, lo que permite crear increíbles efectos visuales en juegos y en otras aplicaciones:
Sombras de texto en <canvas>
Efectos de texto similares a CSS en <canvas> que permiten crear máscaras de recorte, buscar estadísticas en <canvas> y utilizar la propiedad de sombra
Efectos combinados: neón-arcoíris, cebra-reflejo
Efectos de texto similares a los de Photoshop en ejemplos de <canvas> sobre el uso de globalCompositeOperation, createLinearGradient o createPattern 
Sombras internas y externas en <canvas>
Una función poco conocida: el uso de rebobinado en el sentido y en el sentido contrario de las agujas del reloj para crear el inverso de una sombra (la sombra interior)
Spaceage: efecto generativo
Efecto de texto generativo en <canvas> que utiliza ciclos de color hsl() y window.requestAnimationFrame para crear la sensación de movimiento

Sombras de texto en Canvas

Una de las novedades de las especificaciones de CSS3 que más me gustan (junto con el radio del borde y los gradientes web, entre otros), es la capacidad de crear sombras. Es importante conocer las diferencias entre las sombras de <canvas> y de CSS, concretamente:
CSS utiliza dos métodos: sombra de cuadro para elementos de cuadro, como div, span, etc., y sombra de texto para contenido de texto.
<canvas> dispone de un tipo de sombra que se utiliza para todos los objetos vector: ctx.moveTo, ctx.lineTo, ctx.bezierCurveTo, ctx.quadradicCurveTo, ctx.arc, ctx.rect, ctx.fillText, ctx.strokeText, etc. Para crear una sombra en <canvas>, toca estas cuatro propiedades:
ctx.shadowColor = “red” // cadena
Color de la sombra: RGB, RGBA, HSL, HEX y otras entradas 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
Efecto borroso en la sombra (cuanto mayor sea el valor, más borrosa será la sombra)
Para empezar, veamos cómo <canvas> puede emular los efectos CSS. Al buscar en Google Imágenes “sombra de texto css”, obtuvimos algunas demos increíbles que podíamos emular: Line25Stereoscopic y Shadow 3D.
pastedGraphic_1.png
El efecto 3D estereoscópico (consulta este artículo sobre imágenes de anaglifo) es un ejemplo del gran uso que se puede hacer de una simple línea de código. Con esta línea de CSS, podemos crear la ilusión de profundidad si utilizamos gafas rojo/cian de 3D (con las que se ven las películas 3D):
text-shadow: -0.06em 0 0 red, 0.06em 0 0 cyan;
Hay que tener en cuenta dos cosas al convertir esta cadena a <canvas>:
(1) No hay efecto borroso de sombra (el tercer valor), por lo que no hay motivo para ejecutar la sombra, ya que fillText permite obtener el mismo resultado:
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) <canvas> no admite EM, por lo que se deben convertir a PX. Para encontrar el índice de conversión entre PT, PC, EM, EX, PX, etc., podemos crear un elemento con las mismas propiedades de fuente en DOM y establecer el ancho según el formato que vayamos a medir; o, por ejemplo, para capturar la conversión EM -> PX, podemos medir el elemento DOM con “height: 1em”: el resultado de offsetHeight sería el número de PX que hay en 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;

Cómo prevenir la multiplicación de alfa

En un ejemplo más complejo, como el efecto neón de Line25, se debe utilizar la propiedad shadowBlur para emular el efecto correctamente. Dado que el efecto neón depende de varias sombras, nos encontramos con un problema; en <canvas> cada objeto vector solo puede tener una sombra. Por tanto, para dibujar varias sombras, es necesario dibujar varias versiones superpuestas del texto. Esto tiene como resultado una multiplicación de alfa y, como consecuencia, bordes irregulares.
pastedGraphic_2.png
Intenté ejecutar ctx.fillStyle = “rgba(0,0,0,0)” o “transparent” para ocultar el texto, al mismo tiempo que mostraba la sombra... sin embargo, este intento fue inútil, ya que la sombra nunca puede ser más opaca que fillStyle porque la sombra es una multiplicación del valor alfa de fillStyle.
Afortunadamente, hay una forma de solucionarlo. Podemos dibujar la sombra alejada del texto, manteniendo ambos separados (de forma que no se superpongan) y ocultando así el texto que quede fuera 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);

Cómo recortar alrededor del bloque de texto

Para limpiar esto un poco, podemos añadir una ruta de recorte para evitar que fillText se dibuje en primer lugar (al mismo tiempo que permitimos que se dibuje la sombra). Para poder crear una ruta de recorte que rodee el texto, necesitamos conocer el ancho y la altura del texto (llamada “altura Mt” por ser históricamente la altura de la letra “M” en las imprentas). Podemos obtener el ancho con ctx.measureText().width, sin embargo, ctx.measureText().height no existe.
Afortunadamente, gracias a este truco CSS (consulta este artículo sobre medidas tipográficas para conocer más formas de corregir antiguas implementaciones de <canvas> con medidas CSS), podemos calcular la altura del texto midiendo el valor de offsetHeight de un <intervalo> 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 aquí, podemos crear un rectángulo para utilizarlo como ruta de recorte, encuadrando la “sombra” mientras eliminamos la forma simulada.
ctx.rect(0, 0, width, emHeight);
ctx.clip();
Si lo intentamos todo a la vez y lo optimizamos (si una sombra no tiene efecto borroso, se puede utilizar fillText para conseguir el mismo efecto, ahorrándonos el tener que configurar 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);
    
Introducir todos estos comandos de <canvas> manualmente es muy tedioso, por lo que he incluido un sencillo analizador de sombra de texto en el código fuente de la demo que permite convertir los comandos CSS a comandos <canvas>. Ahora, nuestros elementos <canvas> tienen toda una gama de estilos que se pueden utilizar. Estos mismos efectos de sombra se pueden utilizar en cualquier objeto vector, desde WebFonts hasta formas complejas importadas de SVG, para generar formas de vector, etc.
pastedGraphic_3.png

Imágenes 3D

Al escribir esta sección del artículo, el ejemplo estereoscópico despertó mi curiosidad.  ¿Sería muy difícil crear un efecto de pantalla de película 3D usando <canvas> y dos imágenes tomadas desde perspectivas ligeramente diferentes?  Aparentemente, no.  El siguiente kernel combina el canal rojo de la primera imagen (data) 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;
Visita la demo de Stereoscopic para ver cómo crear imágenes para mejorarlas con gafas 3D (cian/magenta).  Ya solo faltaría que alguien se pegase dos iPhones con cinta adhesiva en la frente y pulsara el botón de “grabar vídeo” al mismo tiempo para poder hacer una película 3D con HTML5.  ¿Algún voluntario?
pastedGraphic_5.png
Gafas 3D

Efectos combinados: neón-arcoíris, cebra-reflejo

Combinar varios efectos en <canvas> es fácil, pero es necesario disponer de algunos conocimientos básicos de globalCompositeOperation (GCO). En comparación con las operaciones de GIMP (o Photoshop), existen 12 GCO en <canvas>. Dos de ellos, darker y lighter se pueden considerar modos de fusión de capas; las otras 10 operaciones se aplican a las capas como máscaras alfa (una capa elimina los píxeles de la otra). globalCompositeOperation une las “capas” (o, en nuestro caso, las cadenas de código), combinándolas de formas nuevas y emocionantes:
pastedGraphic_6.png
Mi modo favorito es globalCompositeOperation=”lighter”.  El modo lighter mezcla los píxeles añadidos de forma similar a como se mezcla la luz; cuando las luces roja, verde y azul están en su máxima intensidad, vemos luz blanca. Es increíble cuando la pruebas, especialmente cuando se establece en <canvas> un valor bajo para globalAlpha, permitiendo un control más preciso y unos bordes más regulares. Al modo lighter se le ha dado muchos usos. De los más recientes, mi favorito es un creador de fondos de escritorio en HTML5 que encontré en http://weavesilk.com/. Una de mis demos, Breathing Galaxies (JS1k), también utiliza el modo lighter. En los patrones de dibujo de estos dos ejemplos se puede empezar a ver los efectos que puede crear este modo.

Efecto de vibración neón-arcoíris

Un efecto neón-arcoíris-resplandor similar al de Photoshop con un contorno vibrante, al combinar efectos mediante globalCompositeOperation (source-in, lighter y darker). Esta demo es una progresión de la demo de “sombras de texto en <canvas>”.
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 reflejo-cebra

El efecto reflejo-cebra se inspiró en el excelente recurso de WebDesignerWall sobre cómo animar tu página con CSS. Ahora llevamos la idea un poco más lejos, creando un “reflejo” para el texto, tal y como se vería en iTunes. El efecto combina fillColor (blanco), createPattern (zebra.png) y linearGradient (brillo); esto ilustra la capacidad de aplicar varios tipos de relleno para cada objeto vector:
pastedGraphic_8.png
Ver efecto reflejo 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 internas/externas en Canvas

Las especificaciones de <canvas> no tratan el tema de las sombras “internas” y “externas”. De hecho, a primera vista, da la sensación de que las sombras “internas” no son compatibles. Este no es el caso. Simplemente es un poco difícil de habilitar ;) Como se propuso en una entrada reciente del blog F1LT3R, puedes crear sombras internas mediante las propiedades únicas de las reglas de rebobinado en el sentido y en el sentido contrario de las agujas del reloj. Para hacerlo, hay que dibujar un rectángulo contenedor para crear una “sombra interna” y, a continuación, con las reglas de rebobinado opuestas, dibujar una forma de recorte para crear el inverso de la forma.
El siguiente ejemplo permite que la sombra interna y fillStyle apliquen el estilo color+gradiente+patrón de forma simultánea. Puedes especificar la rotación del patrón individualmente; observa que las rayas de cebra ahora son perpendiculares entre sí. Al utilizar una máscara de recorte del tamaño del cuadro contenedor, se elimina la necesidad de un contenedor de gran tamaño para abarcar toda la forma de recorte, mejorando así la velocidad al evitar que se procesen las partes innecesarias de la sombra.
pastedGraphic_9.png
Ver efecto 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();
};
En estos ejemplos se puede observar que con globalCompositeOperation podemos combinar efectos, creando efectos más elaborados (mediante enmascarados y fusión). La pantalla es tu ostra ;)

Spaceage, efectos generativos

En <canvas>, es posible pasar del carácter Unicode 0x2708:
pastedGraphic_10.png
... a este ejemplo con sombra:
pastedGraphic_11.png
... ejecutando varias veces ctx.strokeText() con un valor bajo de lineWidth (0,25), al mismo tiempo que se reduce lentamente el desplazamiento de x y alfa, lo que daría a los elementos de nuestro vector la sensación de movimiento.
Si representamos la posición XY de los elementos en una onda seno/coseno y cambiamos de color mediante la propiedad HSL, podemos crear efectos más interesantes, como este ejemplo de “riesgo biológico”:
pastedGraphic_12.png
TOMADO DE:https://www.html5rocks.com/es/tutorials/canvas/texteffects/ 

No hay comentarios:

Publicar un comentario