API de Paint de CSS

Nuevas posibilidades en Chrome 65

La API de CSS Paint (también conocida como "CSS Custom Paint" o "Houdini's Paint worklet") está habilitada de forma predeterminada a partir de Chrome 65. ¿Qué representan? ¿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 de CSS integradas, como linear-gradient(). En lugar de usarlos, ahora puedes usar paint(myPainter) para hacer referencia a un worklet de pintura.

Cómo escribir un worklet de pintura

Para definir un worklet de pintura llamado myPainter, debemos cargar un archivo de este tipo 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, ya que lo conocemos por <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 está a nuestra disposición. properties Las explicaré más adelante en este artículo.

Como ejemplo introductorio, escribiremos un worklet de pintura de tablero de ajedrez y lo usaremos 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 damas 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 genial, pero también es bastante estático. ¿Quisiéramos escribir un nuevo worklet cada vez que quisiéramos el mismo patrón, pero con cuadrados de diferentes tamaños? La respuesta es no.

Parametrización de 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 das a la clase un atributo inputProperties estático, te puedes suscribir a los cambios de cualquier propiedad de CSS, incluidas las personalizadas. Se te proporcionarán los valores 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 diferentes de tableros de ajedrez. Pero aún mejor, ahora podemos ir a Herramientas para desarrolladores y abordar los valores hasta encontrar el aspecto correcto.

Navegadores que no admiten el worklet 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 sección Is Houdini Ready?. Mientras tanto, asegúrate de usar la mejora progresiva para que el código se siga ejecutando incluso si no es compatible con el trabajo de pintura. Para asegurarte de que todo funcione como se espera, debes ajustar tu código en dos lugares: CSS y JS.

La detección de 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:

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

Un truco más compacto es usar el hecho de que CSS invalida y, luego, ignora una declaración de propiedad completa si contiene una función desconocida. Si especificas una propiedad dos veces, primero sin el worklet de pintura y, luego, con el worklet de pintura, obtendrás una mejora progresiva:

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

En navegadores compatibles con el worklet de pintura, la segunda declaración de background-image reemplazará la primera. En navegadores no compatibles con el worklet de pintura, la segunda declaración no es válida y se descartará, lo que dejará la primera en vigencia.

Polyfill de pintura de CSS

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

Casos de uso

Hay muchos casos de uso para los worklets de pintura, algunos de ellos más obvios que otros. Una de las más evidentes es usar el worklet de pintura para reducir el tamaño de tu DOM. A menudo, los elementos se agregan exclusivamente para crear adornos con CSS. Por ejemplo, en Material Design Lite, el botón con el efecto de ondas contiene 2 elementos <span> adicionales para implementar el efecto de ondas. 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 cambio, implementas el efecto de ondas con el worklet de pintura, obtendrás 0 elementos adicionales y solo un worklet de pintura. Además, tienes algo que es mucho más fácil de personalizar y parametrizar.

Otra ventaja de usar el worklet de pintura es que, en la mayoría de las situaciones, una solución con el worklet de pintura es pequeña en términos de bytes. Por supuesto, hay una desventaja: tu código de pintura se ejecutará cada vez que cambie el tamaño del lienzo o alguno 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 conjuntos de datos de pintura del subproceso principal, de modo que incluso los de pintura de larga duración no afecten la capacidad de respuesta del subproceso principal.

Para mí, el cliente potencial más interesante es que el worklet de pintura permite un polyfill eficaz 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 el worklet de pintura. Un caso de uso menos intuitivo para el worklet de pintura es mask-image para hacer que los elementos del DOM tengan formas arbitrarias. Por ejemplo, un diamante:

Elemento del DOM en forma de diamante.
Un elemento del DOM con forma de diamante.

mask-image toma una imagen del tamaño del elemento. Áreas en las que la imagen de 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

El worklet de pintura está en Chrome Canary desde hace un tiempo. En Chrome 65, está habilitado 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.