API de Paint de CSS

Nuevas posibilidades en Chrome 65

La API de CSS Paint (también conocida como "CSS Custom Paint" o "worklet de pintura de Houdini") se habilita de forma predeterminada a partir de Chrome 65. ¿De qué se trata? ¿Qué puedes hacer con él? ¿Cómo funciona? Bueno, sigue leyendo, ¿sí?

La API de pintura de CSS te permite generar una imagen de manera programática cada vez que una propiedad de CSS espera una imagen. Por lo general, las propiedades como background-image o border-image se usan con url() para cargar un archivo de imagen o con funciones integradas de CSS, como linear-gradient(). En lugar de usar esos, ahora puedes usar paint(myPainter) para hacer referencia a una worklet de pintura.

Cómo escribir una tarea de pintura

Para definir una tarea de pintura llamada myPainter, debemos cargar un archivo de tarea de pintura de CSS con CSS.paintWorklet.addModule('my-paint-worklet.js'). En ese archivo, podemos usar la función registerPaint para registrar una clase de worklet de pintura:

class MyPainter {
  paint(ctx, geometry, properties) {
    // ...
  }
}

registerPaint('myPainter', MyPainter);

Dentro de la devolución de llamada paint(), podemos usar ctx de la misma manera que lo haríamos con un CanvasRenderingContext2D, como lo conocemos de <canvas>. Si sabes cómo dibujar en un <canvas>, puedes dibujar en un worklet de pintura. geometry nos indica el ancho y la altura del lienzo que tenemos a nuestra disposición. properties Las explicaré más adelante en este artículo.

Como ejemplo introductorio, escribamos una tarea de pintura de tablero de ajedrez y usémosla como imagen de fondo de un <textarea>. (Estoy usando un área de texto porque se puede cambiar de tamaño de forma predeterminada).

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  CSS.paintWorklet.addModule('checkerboard.js');
</script>
// checkerboard.js
class CheckerboardPainter {
  paint(ctx, geom, properties) {
    // Use `ctx` as if it was a normal canvas
    const colors = ['red', 'green', 'blue'];
    const size = 32;
    for(let y = 0; y < geom.height/size; y++) {
      for(let x = 0; x < geom.width/size; x++) {
        const color = colors[(x + y) % colors.length];
        ctx.beginPath();
        ctx.fillStyle = color;
        ctx.rect(x * size, y * size, size, size);
        ctx.fill();
      }
    }
  }
}

// Register our class under a specific name
registerPaint('checkerboard', CheckerboardPainter);

Si ya usaste <canvas>, este código debería resultarte familiar. Mira la demostración en vivo aquí.

Área de texto con un patrón de tablero de ajedrez como imagen de fondo
Área de texto con un patrón de tablero de ajedrez como imagen de fondo.

La diferencia de usar una imagen de fondo común aquí es que el patrón se volverá a dibujar a pedido, cada vez que el usuario cambie el tamaño del área de texto. Esto significa que la imagen de fondo siempre es exactamente tan grande como debe ser, incluida la compensación de las pantallas de alta densidad.

Eso es muy interesante, pero también es bastante estático. ¿Querríamos escribir una nueva worklet cada vez que quisiéramos el mismo patrón, pero con cuadrados de tamaño diferente? La respuesta es no.

Parametriza tu worklet

Afortunadamente, el worklet de pintura puede acceder a otras propiedades de CSS, que es donde entra en juego el parámetro adicional properties. Si le asignas a la clase un atributo inputProperties estático, puedes suscribirte a los cambios en cualquier propiedad CSS, incluidas las propiedades personalizadas. Los valores se te proporcionarán a través del parámetro properties.

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    /* The paint worklet subscribes to changes of these custom properties. */
    --checkerboard-spacing: 10;
    --checkerboard-size: 32;
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  CSS.paintWorklet.addModule('checkerboard.js');
</script>
// checkerboard.js
class CheckerboardPainter {
  // inputProperties returns a list of CSS properties that this paint function gets access to
  static get inputProperties() { return ['--checkerboard-spacing', '--checkerboard-size']; }

  paint(ctx, geom, properties) {
    // Paint worklet uses CSS Typed OM to model the input values.
    // As of now, they are mostly wrappers around strings,
    // but will be augmented to hold more accessible data over time.
    const size = parseInt(properties.get('--checkerboard-size').toString());
    const spacing = parseInt(properties.get('--checkerboard-spacing').toString());
    const colors = ['red', 'green', 'blue'];
    for(let y = 0; y < geom.height/size; y++) {
      for(let x = 0; x < geom.width/size; x++) {
        ctx.fillStyle = colors[(x + y) % colors.length];
        ctx.beginPath();
        ctx.rect(x*(size + spacing), y*(size + spacing), size, size);
        ctx.fill();
      }
    }
  }
}

registerPaint('checkerboard', CheckerboardPainter);

Ahora podemos usar el mismo código para todos los tipos de tableros de ajedrez. Pero aún mejor, ahora podemos ir a Herramientas para desarrolladores y trabajar con los valores hasta encontrar el aspecto correcto.

Navegadores que no admiten la función de trabajo de pintura

Al momento de la redacción, solo Chrome tiene implementado el worklet de pintura. Si bien hay indicadores positivos de todos los demás proveedores de navegadores, no hay mucho progreso. Para mantenerte al día, consulta periódicamente la información sobre Is Houdini Ready?. Mientras tanto, asegúrate de usar la mejora progresiva para mantener el código en ejecución, incluso si no hay compatibilidad con la tarea de pintura. Para asegurarte de que todo funcione como se espera, debes ajustar el código en dos lugares: el CSS y el JS.

La detección de la compatibilidad con el worklet de pintura en JS se puede realizar verificando el objeto CSS: js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } En el lado de CSS, tienes dos opciones. Puedes usar @supports de las siguientes maneras:

@supports (background: paint(id)) {
  /* ... */
}

Un truco más compacto es usar el hecho de que CSS invalida y, posteriormente, ignora una declaración de propiedad completa si hay una función desconocida en ella. Si especificas una propiedad dos veces (primero sin la tarea de pintura y, luego, con la tarea de pintura), obtienes la mejora progresiva:

textarea {
  background-image: linear-gradient(0, red, blue);
  background-image: paint(myGradient, red, blue);
}

En los navegadores con compatibilidad con la tarea de pintura, la segunda declaración de background-image reemplazará a la primera. En los navegadores sin compatibilidad con la tarea de pintura, la segunda declaración no es válida y se descartará, por lo que la primera declaración seguirá vigente.

Compensación de pintura CSS

Para muchos usos, también es posible usar el polyfill de pintura de CSS, que agrega compatibilidad con pintura personalizada de CSS y Paint Worklets a los navegadores modernos.

Casos de uso

Existen muchos casos de uso para los worklets de pintura, algunos de los cuales son más obvios que otros. Una de las más obvias es usar la tarea de pintura para reducir el tamaño de tu DOM. A menudo, los elementos se agregan solo para crear adornos con CSS. Por ejemplo, en Material Design Lite, el botón con el efecto de onda contiene 2 elementos <span> adicionales para implementar la onda en sí. Si tienes muchos botones, esto puede sumar una gran cantidad de elementos del DOM y puede degradar el rendimiento en dispositivos móviles. Si, en su lugar, implementas el efecto de onda con la función worklet de pintura, obtendrás 0 elementos adicionales y solo una función worklet de pintura. Además, tienes algo que es mucho más fácil de personalizar y parametrizar.

Otra ventaja de usar la tarea de pintura es que, en la mayoría de los casos, una solución que usa la tarea de pintura es pequeña en términos de bytes. Por supuesto, hay una compensación: tu código de pintura se ejecutará cada vez que cambie el tamaño del lienzo o cualquiera de los parámetros. Por lo tanto, si tu código es complejo y tarda mucho, es posible que genere un bloqueo. Chrome está trabajando para quitar los worklets de pintura del subproceso principal, de modo que incluso los worklets de pintura de larga duración no afecten la capacidad de respuesta del subproceso principal.

Para mí, la perspectiva más emocionante es que la worklet de pintura permite un polyfill eficiente de las funciones de CSS que un navegador aún no tiene. Un ejemplo sería polarizar gradientes cónicos hasta que lleguen a Chrome de forma nativa. Otro ejemplo: en una reunión de CSS, se decidió que ahora se pueden tener varios colores de borde. Durante la reunión, mi colega Ian Kilpatrick escribió un polyfill para este comportamiento de CSS nuevo con el worklet de pintura.

Pensar de manera innovadora.

La mayoría de las personas comienzan a pensar en las imágenes de fondo y de borde cuando aprenden sobre la worklet de pintura. Un caso de uso menos intuitivo para la tarea de pintura es mask-image para hacer que los elementos del DOM tengan formas arbitrarias. Por ejemplo, un diamante:

Un elemento DOM con forma de rombo.
Es un elemento DOM con forma de rombo.

mask-image toma una imagen del tamaño del elemento. En las áreas en las que la imagen de la máscara es transparente, el elemento es transparente. Áreas en las que la imagen de la máscara es opaca, el elemento opaco

Ahora en Chrome

La tarea de Paint está disponible en Chrome Canary desde hace un tiempo. Con Chrome 65, está habilitada de forma predeterminada. ¡Sigue adelante y prueba las nuevas posibilidades que abre el worklet de pintura y muéstranos lo que creaste! Para obtener más inspiración, consulta la colección de Vincent De Oliveira.