Siempre fuiste tú, Canvas2D

Aaron Krajeski
Aaron Krajeski

En un mundo de sombreadores, mallas y filtros, es posible que Canvas2D no te entusiasme. Pero debería. Entre el 30 y el 40% de las páginas web tienen un elemento <canvas>, y el 98% de todos los lienzos usan un contexto de renderización de Canvas2D. Hay Canvas2D en automóviles, refrigeradores, y en el espacio (en serio).

Es cierto que la API está un poco desactualizada en lo que respecta al dibujo 2D de última generación. Por suerte, trabajamos arduamente para implementar nuevas funciones en Canvas2D para ponernos al día con CSS, optimizar la ergonomía y mejorar el rendimiento.

Parte 1: ponte al día con CSS

CSS tiene algunos comandos de dibujo que faltan en Canvas2D. Con la nueva API, agregamos algunas de las funciones más solicitadas:

Rectángulo redondo

Rectángulos redondeados: la piedra angular de Internet, de la informática, de la civilización.

En serio, los rectángulos redondeados son muy útiles: como botones, burbujas de chat, miniaturas, globos de diálogo, lo que sea. Siempre se puede hacer un rectángulo redondeado en Canvas2D, simplemente ha sido un poco desordenado:

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'magenta';

const top = 10;
const left = 10;
const width = 200;
const height = 100;
const radius = 20;

ctx.beginPath();
ctx.moveTo(left + radius, top);
ctx.lineTo(left + width - radius, top);
ctx.arcTo(left + width, top, left + width, top + radius, radius);
ctx.lineTo(left + width, top + height - radius);
ctx.arcTo(left + width, top + height, left + width - radius, top + height, radius);
ctx.lineTo(left + radius, top + height);
ctx.arcTo(left, top + height, left, top + height - radius, radius);
ctx.lineTo(left, top + radius);
ctx.arcTo(left, top, left + radius, top, radius);
ctx.stroke();

Todo esto era necesario para un rectángulo redondeado modesto y simple:

Un rectángulo redondeado.

Con la nueva API, hay un método roundRect().

ctx.roundRect(upper, left, width, height, borderRadius);

Por lo tanto, lo anterior se puede reemplazar por completo por lo siguiente:

ctx.roundRect(10, 10, 200, 100, 20);

El método ctx.roundRect() también toma un array para el argumento borderRadius de hasta cuatro números. Estos radios controlan las cuatro esquinas del rectángulo redondeado de la misma manera que CSS. Por ejemplo:

ctx.roundRect(10, 10, 200, 100, [15, 50, 30]);

Mira la demostración para probarla.

Gradiente cónico

Has visto gradientes lineales:

const gradient = ctx.createLinearGradient(0, 0, 200, 100);
gradient.addColorStop(0, 'blue');
gradient.addColorStop(0.5, 'magenta');
gradient.addColorStop(1, 'white');
ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);

Gradiente lineal.

Gradientes radiales:

const radialGradient = ctx.createRadialGradient(150, 75, 10, 150, 75, 70);
radialGradient.addColorStop(0, 'white');
radialGradient.addColorStop(0.5, 'magenta');
radialGradient.addColorStop(1, 'lightblue');

ctx.fillStyle = radialGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);

Gradiente radial.

Pero ¿qué tal un buen gradiente cónico?

const grad = ctx.createConicGradient(0, 100, 100);

grad.addColorStop(0, 'red');
grad.addColorStop(0.25, 'orange');
grad.addColorStop(0.5, 'yellow');
grad.addColorStop(0.75, 'green');
grad.addColorStop(1, 'blue');

ctx.fillStyle = grad;
ctx.fillRect(0, 0, 200, 200);

Un gradiente cónico.

Modificadores de texto

Las capacidades de renderización de texto de Canvas2D se retrasaron bastante. Chrome agregó varios atributos nuevos al procesamiento de texto en Canvas2D:

Todos estos atributos coinciden con sus contrapartes de CSS con los mismos nombres.

Parte 2: ajustes ergonómicos

Anteriormente, era posible realizar algunas tareas con Canvas2D, pero su implementación era innecesariamente complicada. A continuación, se incluyen algunas mejoras en la calidad de vida para los desarrolladores de JavaScript que desean usar Canvas2D:

Se restableció el contexto.

Para explicar cómo borrar un lienzo, escribí una pequeña función para dibujar un patrón retro:

draw90sPattern();

Un patrón retro de triángulos y cuadrados.

¡Genial! Ahora que terminé con ese patrón, quiero borrar el lienzo y dibujar otra cosa. Espera, ¿cómo volvemos a borrar un lienzo? ¡Por supuesto! ctx.clearRect(), por supuesto.

ctx.clearRect(0, 0, canvas.width, canvas.height);

Huh… eso no funcionó. ¡Por supuesto! Primero debo restablecer la transformación:

ctx.resetTransform();
ctx.clearRect(0, 0, canvas.width, canvas.height);
Un lienzo en blanco.

¡Perfecto! Un buen lienzo en blanco. Ahora comencemos a dibujar una línea horizontal bonita:

ctx.moveTo(10, 10);
ctx.lineTo(canvas.width, 10);
ctx.stroke();

Una línea horizontal y una diagonal.

¡Grrrr! Respuesta incorrecta 😡 ¿Qué hace esa línea adicional aquí? Además, ¿por qué es rosa? De acuerdo, vamos a revisar StackOverflow.

canvas.width = canvas.width;

¿Por qué es tan? ¿Por qué es tan difícil?

Bueno, ya no. Con la nueva API, tenemos lo siguiente:

ctx.reset();

Lamentamos la demora.

Filtros

Los filtros SVG son un mundo aparte. Si es la primera vez que los usas, te recomiendo que leas The Art Of SVG Filters And Why It Is Awesome, que muestra parte de su increíble potencial.

Los filtros de estilo SVG ya están disponibles para Canvas2D. Solo debes pasar el filtro como una URL que apunte a otro elemento de filtro de SVG en la página:

<svg>
  <defs>
    <filter id="svgFilter">
      <feGaussianBlur in="SourceGraphic" stdDeviation="5" />
      <feConvolveMatrix kernelMatrix="-3 0 0 0 0.5 0 0 0 3" />
      <feColorMatrix type="hueRotate" values="90" />
    </filter>
  </defs>
</svg>
const canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 400;
const ctx = canvas.getContext('2d');
document.body.appendChild(canvas);

ctx.filter = "url('#svgFilter')";
draw90sPattern(ctx);

Lo cual desordena bastante bien nuestro patrón:

El patrón retro con un efecto de desenfoque aplicado

Pero, ¿qué sucede si quieres hacer lo anterior, pero permanecer en JavaScript y no jugar con cadenas? Con la nueva API, esto es totalmente posible.

ctx.filter = new CanvasFilter([
  { filter: 'gaussianBlur', stdDeviation: 5 },
  {
    filter: 'convolveMatrix',
    kernelMatrix: [
      [-3, 0, 0],
      [0, 0.5, 0],
      [0, 0, 3],
    ],
  },
  { filter: 'colorMatrix', type: 'hueRotate', values: 90 },
]);

Es muy fácil. Pruébala y juega con los parámetros que se muestra en la demostración.

Parte 3: Mejoras en el rendimiento

Con la nueva API de Canvas2D, también queríamos mejorar el rendimiento siempre que fuera posible. Agregamos algunas funciones para brindarles a los desarrolladores un control más detallado de sus sitios web y permitir las velocidades de fotogramas más fluidas posibles:

Se leerá con frecuencia

Usa getImageData() para volver a leer los datos de píxeles de un lienzo. Puede ser muy lento. La nueva API te brinda una forma de marcar explícitamente un lienzo para volver a leerlo (por ejemplo, para efectos generativos). Esto te permite optimizar los elementos internos y mantener el lienzo rápido para una mayor variedad de casos de uso. Esta función está en Firefox por un tiempo y, por último, la incorporaremos a la especificación del lienzo.

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });

Pérdida de contexto

¡Hagamos que las pestañas tristes vuelvan a ser felices! En caso de que un cliente se quede sin memoria de GPU o se produzca algún otro desastre en tu lienzo, ahora puedes recibir una devolución de llamada y volver a dibujar según sea necesario:

const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

canvas.addEventListener('contextlost', onContextLost);
canvas.addEventListener('contextrestored', redraw);

Si quieres obtener más información sobre el contexto y la pérdida del lienzo, QUÉWG tiene una buena explicación en su wiki.

Conclusión

Ya sea que sea la primera vez que usas Canvas2D, lo hayas usado durante años o hayas evitado usarlo durante años, te recomiendo que le des otra oportunidad. Es la API de al lado que estuvo allí todo el tiempo.