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…

La API de CSS Paint te permite generar una imagen de forma 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 dibujar en un <canvas>, puedes dibujar en una tarea de Paint. geometry nos indica el ancho y la altura del lienzo que tenemos a nuestra disposición. properties Te lo 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 usaste <canvas> en el pasado, 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 con el uso de 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 textarea. Esto significa que la imagen de fondo siempre es exactamente del tamaño que debe ser, incluida la compensación para 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

Por suerte, la worklet de pintura puede acceder a otras propiedades CSS, y es ahí 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 lo mejor es que ahora podemos ir a DevTools y jugar con los valores hasta encontrar el aspecto correcto.

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

En el momento de escribir este artículo, solo Chrome implementó la tarea de trabajo de pintura. Si bien hay indicadores positivos de todos los demás proveedores de navegadores, no hay mucho progreso. Para mantenerte al tanto, consulta ¿Está listo Houdini? con frecuencia. 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.

Para detectar la compatibilidad con la función de trabajo de pintura en JS, puedes verificar el objeto CSS: js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } En el lado del 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 contiene una función desconocida. 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 propia onda. Si tienes muchos botones, esto puede sumar una gran cantidad de elementos DOM y provocar un rendimiento degradado 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 worklet de pintura es que, en la mayoría de los casos, una solución que usa la worklet 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 introduzca latencia. 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 usar un polyfill para los gradientes cónicos hasta que lleguen a Chrome de forma nativa. Otro ejemplo: en una reunión de CSS, se decidió que ahora puedes tener varios colores de borde. Mientras esta reunión aún estaba en curso, mi colega Ian Kilpatrick escribió un polyfill para este nuevo comportamiento de CSS con la worklet de pintura.

Piensa de forma creativa

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 también lo es. Áreas en las que la imagen de la máscara es opaca, el elemento es 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. Continúa y prueba las nuevas posibilidades que abre la función de trabajo de pintura y muéstranos lo que creaste. Para obtener más inspiración, consulta la colección de Vincent De Oliveira.